From 8400991239e256741af0681024676fcea3141408 Mon Sep 17 00:00:00 2001 From: nicolasm-dev <71887763+nicolasm-dev@users.noreply.github.com> Date: Thu, 21 Jan 2021 17:40:37 +0100 Subject: [PATCH 01/99] Hide scrollbar of sidebar --- packages/core/src/layout/Sidebar/Bar.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/core/src/layout/Sidebar/Bar.tsx b/packages/core/src/layout/Sidebar/Bar.tsx index d7318de27d..260593de95 100644 --- a/packages/core/src/layout/Sidebar/Bar.tsx +++ b/packages/core/src/layout/Sidebar/Bar.tsx @@ -39,6 +39,8 @@ const useStyles = makeStyles(theme => ({ padding: 0, background: theme.palette.navigation.background, overflowX: 'hidden', + msOverflowStyle: 'none', + scrollbarWidth: 'none', width: sidebarConfig.drawerWidthClosed, transition: theme.transitions.create('width', { easing: theme.transitions.easing.sharp, @@ -47,6 +49,9 @@ const useStyles = makeStyles(theme => ({ '& > *': { flexShrink: 0, }, + '&::-webkit-scrollbar': { + display: 'none', + }, }, drawerOpen: { width: sidebarConfig.drawerWidthOpen, From 21e624ba94116e8a364032e26b3efae733235c24 Mon Sep 17 00:00:00 2001 From: nicolasm-dev <71887763+nicolasm-dev@users.noreply.github.com> Date: Thu, 21 Jan 2021 17:40:37 +0100 Subject: [PATCH 02/99] Hide scrollbar of sidebar --- .changeset/lovely-panthers-peel.md | 8 ++++++++ packages/core/src/layout/Sidebar/Bar.tsx | 5 +++++ 2 files changed, 13 insertions(+) create mode 100644 .changeset/lovely-panthers-peel.md diff --git a/.changeset/lovely-panthers-peel.md b/.changeset/lovely-panthers-peel.md new file mode 100644 index 0000000000..0127a99a57 --- /dev/null +++ b/.changeset/lovely-panthers-peel.md @@ -0,0 +1,8 @@ +--- +'@backstage/core': minor +--- + +Closes #3556 +The scrollbar of collapsed sidebar is now hidden wihtout full screen. + +![image](https://user-images.githubusercontent.com/46953622/105390193-0bfd0080-5c19-11eb-8e86-2161bbe6e8d9.png) diff --git a/packages/core/src/layout/Sidebar/Bar.tsx b/packages/core/src/layout/Sidebar/Bar.tsx index d7318de27d..260593de95 100644 --- a/packages/core/src/layout/Sidebar/Bar.tsx +++ b/packages/core/src/layout/Sidebar/Bar.tsx @@ -39,6 +39,8 @@ const useStyles = makeStyles(theme => ({ padding: 0, background: theme.palette.navigation.background, overflowX: 'hidden', + msOverflowStyle: 'none', + scrollbarWidth: 'none', width: sidebarConfig.drawerWidthClosed, transition: theme.transitions.create('width', { easing: theme.transitions.easing.sharp, @@ -47,6 +49,9 @@ const useStyles = makeStyles(theme => ({ '& > *': { flexShrink: 0, }, + '&::-webkit-scrollbar': { + display: 'none', + }, }, drawerOpen: { width: sidebarConfig.drawerWidthOpen, From d65b34409aae8579631f1c12ac362afba59b3496 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Tue, 12 Jan 2021 16:44:30 -0500 Subject: [PATCH 03/99] add alert hooks --- .../src/alerts/KubernetesMigrationAlert.tsx | 156 +++++++++++++ .../src/alerts/ProjectGrowthAlert.tsx | 45 ++++ .../src/alerts/UnlabeledDataflowAlert.tsx | 43 ++++ .../index.ts} | 22 +- plugins/cost-insights/src/client.ts | 31 ++- .../ActionItemCard.test.tsx} | 8 +- .../components/ActionItems/ActionItemCard.tsx | 62 +++++ .../ActionItems/ActionItems.test.tsx | 86 +++++++ .../components/ActionItems/ActionItems.tsx | 132 +++++++++++ .../src/components/ActionItems/index.ts | 18 ++ .../AlertActionCardList/AlertActionCard.tsx | 45 ---- .../AlertInsights/AlertDialog.test.tsx | 169 ++++++++++++++ .../components/AlertInsights/AlertDialog.tsx | 210 +++++++++++++++++ .../AlertInsights/AlertInsights.test.tsx | 89 ++++++++ .../AlertInsights/AlertInsights.tsx | 212 ++++++++++++++++-- .../AlertInsights/AlertInsightsHeader.tsx | 5 +- .../AlertInsightsSection.test.tsx | 104 ++++++++- .../AlertInsights/AlertInsightsSection.tsx | 68 +++++- .../AlertInsightsSectionHeader.tsx | 39 ++-- .../AlertInsights/AlertStatusSummary.test.tsx | 58 +++++ .../AlertInsights/AlertStatusSummary.tsx | 116 ++++++++++ .../AlertStatusSummaryButton.tsx | 54 +++++ .../CostInsightsNavigation.tsx | 5 +- .../CostInsightsPage/CostInsightsPage.tsx | 84 ++++--- .../CostInsightsPage/CostInsightsPageRoot.tsx | 5 +- .../CostOverviewCard/CostOverviewCard.tsx | 4 +- .../MigrationAlertCard/MigrationAlertCard.tsx | 60 +++++ .../MigrationAlertCard/MigrationBarChart.tsx | 54 +++++ .../MigrationBarChartLegend.tsx | 58 +++++ .../index.ts | 2 +- .../ProductInsightsCard.test.tsx | 11 - .../ProductInsightsCard.tsx | 6 +- .../ProjectGrowthInstructionsPage.tsx | 2 +- .../src/forms/AlertAcceptForm.tsx | 57 +++++ .../src/forms/AlertDismissForm.tsx | 156 +++++++++++++ .../src/forms/AlertSnoozeForm.tsx | 115 ++++++++++ .../src/forms/MigrationDismissForm.tsx | 108 +++++++++ plugins/cost-insights/src/forms/index.ts | 21 ++ plugins/cost-insights/src/hooks/index.ts | 1 + plugins/cost-insights/src/hooks/useAlerts.tsx | 76 +++++++ plugins/cost-insights/src/hooks/useScroll.tsx | 63 +++--- plugins/cost-insights/src/index.ts | 2 +- plugins/cost-insights/src/types/Alert.ts | 128 ++++++++++- plugins/cost-insights/src/types/Duration.ts | 1 + plugins/cost-insights/src/utils/alerts.tsx | 58 ++--- plugins/cost-insights/src/utils/duration.ts | 13 +- plugins/cost-insights/src/utils/loading.ts | 1 + plugins/cost-insights/src/utils/styles.ts | 41 +++- plugins/cost-insights/src/utils/tests.tsx | 31 ++- 49 files changed, 2669 insertions(+), 266 deletions(-) create mode 100644 plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx create mode 100644 plugins/cost-insights/src/alerts/ProjectGrowthAlert.tsx create mode 100644 plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.tsx rename plugins/cost-insights/src/{components/AlertActionCardList/AlertActionCardList.tsx => alerts/index.ts} (50%) rename plugins/cost-insights/src/components/{AlertActionCardList/AlertActionCard.test.tsx => ActionItems/ActionItemCard.test.tsx} (87%) create mode 100644 plugins/cost-insights/src/components/ActionItems/ActionItemCard.tsx create mode 100644 plugins/cost-insights/src/components/ActionItems/ActionItems.test.tsx create mode 100644 plugins/cost-insights/src/components/ActionItems/ActionItems.tsx create mode 100644 plugins/cost-insights/src/components/ActionItems/index.ts delete mode 100644 plugins/cost-insights/src/components/AlertActionCardList/AlertActionCard.tsx create mode 100644 plugins/cost-insights/src/components/AlertInsights/AlertDialog.test.tsx create mode 100644 plugins/cost-insights/src/components/AlertInsights/AlertDialog.tsx create mode 100644 plugins/cost-insights/src/components/AlertInsights/AlertInsights.test.tsx create mode 100644 plugins/cost-insights/src/components/AlertInsights/AlertStatusSummary.test.tsx create mode 100644 plugins/cost-insights/src/components/AlertInsights/AlertStatusSummary.tsx create mode 100644 plugins/cost-insights/src/components/AlertInsights/AlertStatusSummaryButton.tsx create mode 100644 plugins/cost-insights/src/components/MigrationAlertCard/MigrationAlertCard.tsx create mode 100644 plugins/cost-insights/src/components/MigrationAlertCard/MigrationBarChart.tsx create mode 100644 plugins/cost-insights/src/components/MigrationAlertCard/MigrationBarChartLegend.tsx rename plugins/cost-insights/src/components/{AlertActionCardList => MigrationAlertCard}/index.ts (90%) create mode 100644 plugins/cost-insights/src/forms/AlertAcceptForm.tsx create mode 100644 plugins/cost-insights/src/forms/AlertDismissForm.tsx create mode 100644 plugins/cost-insights/src/forms/AlertSnoozeForm.tsx create mode 100644 plugins/cost-insights/src/forms/MigrationDismissForm.tsx create mode 100644 plugins/cost-insights/src/forms/index.ts create mode 100644 plugins/cost-insights/src/hooks/useAlerts.tsx diff --git a/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx b/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx new file mode 100644 index 0000000000..8940322223 --- /dev/null +++ b/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx @@ -0,0 +1,156 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import pluralize from 'pluralize'; +import { MigrationAlertCard } from '../components/MigrationAlertCard'; +import { CostInsightsApi } from '../api'; +import { + Alert, + AlertForm, + AlertOptions, + AlertStatus, + AlertSnoozeFormData, + ChangeStatistic, + Entity, +} from '../types'; +import { MigrationDismissForm, MigrationDismissFormData } from '../forms'; + +export interface MigrationData { + startDate: string; + endDate: string; + change: ChangeStatistic; + services: Array; +} + +export interface MigrationAlert extends Alert { + api: CostInsightsApi; + data: MigrationData; +} + +/** + * The alert below is an example of an Alert implementation using event hooks. + * + * Alerts can be customized to be accepted, dismissed snoozed or any combination + * by defining a corresponding hook on the alert instance. + * + * For example, defining an onDismissed hook will render a dismiss button that, when clicked, will + * generate a dialog prompting the user to provide a reason for dismissing the alert. + * Dismiss form data will be passed to the hook, which must eventually return a new set of alerts. + * Errors thrown within hooks will generate a snackbar, which can be used to display a + * user-friendly error message. + * + * Cost Insights provides default forms for each hook, which can be overriden by providing a custom form component. + */ + +export class KubernetesMigrationAlert implements MigrationAlert { + api: CostInsightsApi; + data: MigrationData; + + subtitle = + 'Services running on Kubernetes are estimated to save 50% or more compared to Compute Engine.'; + + // Override default dismiss form with custom form component. + // SnoozeForm: AlertForm = MigrationSnoozeForm; + // AcceptForm: AlertForm = MigrationAcceptForm; + DismissForm: AlertForm< + MigrationAlert, + MigrationDismissFormData + > = MigrationDismissForm; + + constructor(api: CostInsightsApi, data: MigrationData) { + this.api = api; + this.data = data; + } + + get title() { + return `Consider migrating ${pluralize( + 'service', + this.data.services.length, + true, + )} to Kubernetes.`; + } + + get element() { + const subheader = `${pluralize( + 'Compute Engine role', + this.data.services.length, + true, + )}, sorted by cost`; + return ( + + ); + } + + /* Displays a custom dismiss form. */ + async onDismissed( + options: AlertOptions, + ): Promise { + const alerts = await this.api.getAlerts(options.group); + return new Promise(resolve => + setTimeout(resolve, 750, [ + ...alerts.slice(0, 2), + { + title: this.title, + subtitle: this.subtitle, + /** + * If a status property is defined, the alert will be filtered from the action items list + * but still appear grouped with other action items of the same status in the Hidden Action Items section. + */ + status: AlertStatus.Dismissed, + }, + ]), + ); + } + + /* Displays default accept form. */ + async onSnoozed( + options: AlertOptions, + ): Promise { + const alerts = await this.api.getAlerts(options.group); + return new Promise(resolve => + setTimeout(resolve, 750, [ + ...alerts.slice(0, 2), + { + title: this.title, + subtitle: this.subtitle, + status: AlertStatus.Snoozed, + }, + ]), + ); + } + + /* Displays default accept form. */ + async onAccepted(options: AlertOptions): Promise { + const alerts = await this.api.getAlerts(options.group); + return new Promise(resolve => + setTimeout(resolve, 750, [ + ...alerts.slice(0, 2), + { + title: this.title, + subtitle: this.subtitle, + status: AlertStatus.Accepted, + }, + ]), + ); + } +} diff --git a/plugins/cost-insights/src/alerts/ProjectGrowthAlert.tsx b/plugins/cost-insights/src/alerts/ProjectGrowthAlert.tsx new file mode 100644 index 0000000000..67eb1b5071 --- /dev/null +++ b/plugins/cost-insights/src/alerts/ProjectGrowthAlert.tsx @@ -0,0 +1,45 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { ProjectGrowthAlertCard } from '../components/ProjectGrowthAlertCard'; +import { Alert, ProjectGrowthData } from '../types'; + +/** + * The alert below is an example of an Alert implementation; the CostInsightsApi permits returning + * any implementation of the Alert type, so adopters can create their own. The CostInsightsApi + * fetches alert data from the backend, then creates Alert classes with the data. + */ + +export class ProjectGrowthAlert implements Alert { + data: ProjectGrowthData; + + url = '/cost-insights/investigating-growth'; + subtitle = + 'Cost growth outpacing business growth is unsustainable long-term.'; + + constructor(data: ProjectGrowthData) { + this.data = data; + } + + get title() { + return `Investigate cost growth in project ${this.data.project}`; + } + + get element() { + return ; + } +} diff --git a/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.tsx b/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.tsx new file mode 100644 index 0000000000..e889e0e3d4 --- /dev/null +++ b/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.tsx @@ -0,0 +1,43 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { UnlabeledDataflowAlertCard } from '../components/UnlabeledDataflowAlertCard'; +import { Alert, AlertStatus, UnlabeledDataflowData } from '../types'; + +/** + * The alert below is an example of an Alert implementation; the CostInsightsApi permits returning + * any implementation of the Alert type, so adopters can create their own. The CostInsightsApi + * fetches alert data from the backend, then creates Alert classes with the data. + */ + +export class UnlabeledDataflowAlert implements Alert { + data: UnlabeledDataflowData; + status?: AlertStatus; + + url = '/cost-insights/labeling-jobs'; + title = 'Add labels to workflows'; + subtitle = + 'Labels show in billing data, enabling cost insights for each workflow.'; + + constructor(data: UnlabeledDataflowData) { + this.data = data; + } + + get element() { + return ; + } +} diff --git a/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCardList.tsx b/plugins/cost-insights/src/alerts/index.ts similarity index 50% rename from plugins/cost-insights/src/components/AlertActionCardList/AlertActionCardList.tsx rename to plugins/cost-insights/src/alerts/index.ts index aaadbed09d..eb4a079e90 100644 --- a/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCardList.tsx +++ b/plugins/cost-insights/src/alerts/index.ts @@ -13,22 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { Fragment } from 'react'; -import { Paper, Divider } from '@material-ui/core'; -import { AlertActionCard } from './AlertActionCard'; -import { Alert } from '../../types'; -type AlertActionCardList = { - alerts: Array; -}; - -export const AlertActionCardList = ({ alerts }: AlertActionCardList) => ( - - {alerts.map((alert, index) => ( - - - {index < alerts.length - 1 && } - - ))} - -); +export { ProjectGrowthAlert } from './ProjectGrowthAlert'; +export { UnlabeledDataflowAlert } from './UnlabeledDataflowAlert'; +export { KubernetesMigrationAlert } from './KubernetesMigrationAlert'; +export type { MigrationAlert } from './KubernetesMigrationAlert'; diff --git a/plugins/cost-insights/src/client.ts b/plugins/cost-insights/src/client.ts index 0ba57f76fa..837cd7a560 100644 --- a/plugins/cost-insights/src/client.ts +++ b/plugins/cost-insights/src/client.ts @@ -31,7 +31,8 @@ import { import { ProjectGrowthAlert, UnlabeledDataflowAlert, -} from '../src/utils/alerts'; + KubernetesMigrationAlert, +} from '../src/alerts'; import { trendlineOf, changeOf, @@ -174,6 +175,34 @@ export class ExampleCostInsightsClient implements CostInsightsApi { const alerts: Alert[] = await this.request({ group }, [ new ProjectGrowthAlert(projectGrowthData), new UnlabeledDataflowAlert(unlabeledDataflowData), + new KubernetesMigrationAlert(this, { + startDate: '2021-01-24', + endDate: '2020-02-24', + change: { + ratio: 0, + amount: 0, + }, + services: [ + { + id: 'service-a', + aggregation: [20_000, 10_000], + change: { + ratio: -1, + amount: -10_000, + }, + entities: {}, + }, + { + id: 'service-b', + aggregation: [30_000, 15_000], + change: { + ratio: -1, + amount: 15_000, + }, + entities: {}, + }, + ], + }), ]); return alerts; diff --git a/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCard.test.tsx b/plugins/cost-insights/src/components/ActionItems/ActionItemCard.test.tsx similarity index 87% rename from plugins/cost-insights/src/components/AlertActionCardList/AlertActionCard.test.tsx rename to plugins/cost-insights/src/components/ActionItems/ActionItemCard.test.tsx index d896bf0d47..b72f050e5b 100644 --- a/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCard.test.tsx +++ b/plugins/cost-insights/src/components/ActionItems/ActionItemCard.test.tsx @@ -16,9 +16,9 @@ import React from 'react'; import { renderInTestApp } from '@backstage/test-utils'; -import { AlertActionCard } from './AlertActionCard'; +import { ActionItemCard } from './ActionItemCard'; import { MockScrollProvider } from '../../utils/tests'; -import { ProjectGrowthAlert } from '../../utils/alerts'; +import { ProjectGrowthAlert } from '../../alerts'; import { ProjectGrowthData } from '../../types'; const data: ProjectGrowthData = { @@ -31,11 +31,11 @@ const data: ProjectGrowthData = { }; const alert = new ProjectGrowthAlert(data); -describe('', () => { +describe('', () => { it('Renders an alert', async () => { const rendered = await renderInTestApp( - , + 1} /> , ); diff --git a/plugins/cost-insights/src/components/ActionItems/ActionItemCard.tsx b/plugins/cost-insights/src/components/ActionItems/ActionItemCard.tsx new file mode 100644 index 0000000000..3c95fdaa2f --- /dev/null +++ b/plugins/cost-insights/src/components/ActionItems/ActionItemCard.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { MouseEventHandler } from 'react'; +import classnames from 'classnames'; +import { Card, CardHeader } from '@material-ui/core'; +import { useScroll } from '../../hooks'; +import { Alert } from '../../types'; +import { useActionItemCardStyles as useStyles } from '../../utils/styles'; + +type ActionItemCardProps = { + alert: Alert; + number?: number; + avatar?: JSX.Element; + disableScroll?: boolean; +}; + +export const ActionItemCard = ({ + alert, + avatar, + number, + disableScroll = false, +}: ActionItemCardProps) => { + const classes = useStyles(); + const rootClasses = classnames(classes.root, { + [classes.activeRoot]: !disableScroll, + }); + const [, setScroll] = useScroll(); + + const onActionItemClick: MouseEventHandler = () => { + if (!disableScroll && number) { + setScroll(`alert-${number}`); + } + }; + + return ( + + + + ); +}; diff --git a/plugins/cost-insights/src/components/ActionItems/ActionItems.test.tsx b/plugins/cost-insights/src/components/ActionItems/ActionItems.test.tsx new file mode 100644 index 0000000000..8c96e47f1a --- /dev/null +++ b/plugins/cost-insights/src/components/ActionItems/ActionItems.test.tsx @@ -0,0 +1,86 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { ActionItems } from './ActionItems'; +import { MockScrollProvider } from '../../utils/tests'; + +function renderInContext(children: JSX.Element) { + return render({children}); +} + +describe('', () => { + it('should not display status buttons if there no active alerts', () => { + const { queryByRole } = renderInContext( + , + ); + expect(queryByRole('button', { name: 'snoozed' })).not.toBeInTheDocument(); + expect(queryByRole('button', { name: 'accepted' })).not.toBeInTheDocument(); + expect( + queryByRole('button', { name: 'dismissed' }), + ).not.toBeInTheDocument(); + }); + + it('should display status buttons with correct badge number', () => { + const { getByText, getByRole, getAllByText } = renderInContext( + , + ); + expect(getAllByText('1')).toHaveLength(2); // should be a badge of 1 and action item number of 1 + expect(getByText('2')).toBeInTheDocument(); + expect(getByText('3')).toBeInTheDocument(); + expect(getByRole('button', { name: 'snoozed' })).toBeInTheDocument(); + expect(getByRole('button', { name: 'accepted' })).toBeInTheDocument(); + expect(getByRole('button', { name: 'dismissed' })).toBeInTheDocument(); + }); +}); diff --git a/plugins/cost-insights/src/components/ActionItems/ActionItems.tsx b/plugins/cost-insights/src/components/ActionItems/ActionItems.tsx new file mode 100644 index 0000000000..320ebb38aa --- /dev/null +++ b/plugins/cost-insights/src/components/ActionItems/ActionItems.tsx @@ -0,0 +1,132 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { Fragment, MouseEventHandler } from 'react'; +import { + Avatar, + Badge, + Box, + IconButtonProps, + IconButton, + Paper, + Divider, + Tooltip, +} from '@material-ui/core'; +import { default as SnoozeIcon } from '@material-ui/icons/AccessTime'; +import { default as AcceptIcon } from '@material-ui/icons/Check'; +import { default as DismissIcon } from '@material-ui/icons/Delete'; +import { ActionItemCard } from './ActionItemCard'; +import { Alert, AlertStatus } from '../../types'; +import { useScroll, ScrollType } from '../../hooks'; +import { useActionItemCardStyles as useStyles } from '../../utils/styles'; + +type ActionItemsProps = { + active: Alert[]; + snoozed: Alert[]; + accepted: Alert[]; + dismissed: Alert[]; +}; + +export const ActionItems = ({ + active, + snoozed, + accepted, + dismissed, +}: ActionItemsProps) => { + const classes = useStyles(); + const [, setScroll] = useScroll(); + + const isSnoozedButtonDisplayed = !!snoozed.length; + const isAcceptedButtonDisplayed = !!accepted.length; + const isDismissedButtonDisplayed = !!dismissed.length; + const isStatusButtonGroupDisplayed = !!active.length; + + const onStatusButtonClick: MouseEventHandler = () => + setScroll(ScrollType.AlertSummary); + + return ( + <> + + {active.map((alert, index) => ( + + {index + 1}} + /> + {index < active.length - 1 && } + + ))} + + {isStatusButtonGroupDisplayed && ( + + {isAcceptedButtonDisplayed && ( + } + amount={accepted.length} + onClick={onStatusButtonClick} + /> + )} + {isSnoozedButtonDisplayed && ( + } + onClick={onStatusButtonClick} + /> + )} + {isDismissedButtonDisplayed && ( + } + amount={dismissed.length} + onClick={onStatusButtonClick} + /> + )} + + )} + + ); +}; + +type AlertStatusButtonProps = { + title: string; + amount: number; + icon: JSX.Element; + onClick: MouseEventHandler; +} & IconButtonProps; + +const AlertStatusButton = ({ + title, + amount, + icon, + onClick, + ...buttonProps +}: AlertStatusButtonProps) => ( + + + {icon} + + +); diff --git a/plugins/cost-insights/src/components/ActionItems/index.ts b/plugins/cost-insights/src/components/ActionItems/index.ts new file mode 100644 index 0000000000..b63e34b0af --- /dev/null +++ b/plugins/cost-insights/src/components/ActionItems/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ActionItems } from './ActionItems'; +export { ActionItemCard } from './ActionItemCard'; diff --git a/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCard.tsx b/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCard.tsx deleted file mode 100644 index 78feade952..0000000000 --- a/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCard.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import { Avatar, Card, CardHeader } from '@material-ui/core'; -import { useScroll } from '../../hooks'; -import { Alert } from '../../types'; -import { - useAlertActionCardHeader as useHeaderStyles, - useAlertActionCardStyles as useStyles, -} from '../../utils/styles'; - -type AlertActionCardProps = { - alert: Alert; - number: number; -}; - -export const AlertActionCard = ({ alert, number }: AlertActionCardProps) => { - const { scrollIntoView } = useScroll(`alert-${number}`); - const headerClasses = useHeaderStyles(); - const classes = useStyles(); - - return ( - - {number}} - title={alert.title} - subheader={alert.subtitle} - /> - - ); -}; diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertDialog.test.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertDialog.test.tsx new file mode 100644 index 0000000000..5785592aeb --- /dev/null +++ b/plugins/cost-insights/src/components/AlertInsights/AlertDialog.test.tsx @@ -0,0 +1,169 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { AlertDialog } from './AlertDialog'; +import { render } from '@testing-library/react'; +import { + Alert, + AlertFormProps, + AlertSnoozeOptions, + AlertDismissOptions, +} from '../../types'; + +type MockFormDataProps = AlertFormProps; + +const MockForm = React.forwardRef( + (props, ref) => ( +
+ You. Complete. Me. +
+ ), +); + +describe('', () => { + const snoozableAlert: Alert = { + title: 'title', + subtitle: 'test-subtitle', + onSnoozed: jest.fn(), + }; + + const dimissableAlert: Alert = { + title: 'title', + subtitle: 'subtitle', + onDismissed: jest.fn(), + }; + + const customSnoozeAlert: Alert = { + title: 'title', + subtitle: 'subtitle', + onSnoozed: jest.fn(), + SnoozeForm: MockForm, + }; + + const customDismissAlert: Alert = { + title: 'title', + subtitle: 'subtitle', + onDismissed: jest.fn(), + DismissForm: MockForm, + }; + + const customAcceptAlert: Alert = { + title: 'title', + subtitle: 'test-subtitle', + onAccepted: jest.fn(), + AcceptForm: MockForm, + }; + + it('Displays a default snooze form', () => { + const { getByText } = render( + , + ); + expect(getByText('For how long?')).toBeInTheDocument(); + expect(getByText('Snooze this action item?')).toBeInTheDocument(); + expect( + getByText('This action item will be snoozed for all of Ramones.'), + ).toBeInTheDocument(); + AlertSnoozeOptions.forEach(a => + expect(getByText(a.label)).toBeInTheDocument(), + ); + }); + + it('Displays a custom snooze form', () => { + const { getByText } = render( + , + ); + expect(getByText('You. Complete. Me.')).toBeInTheDocument(); + expect(getByText('Snooze this action item?')).toBeInTheDocument(); + expect( + getByText('This action item will be snoozed for all of Ramones.'), + ).toBeInTheDocument(); + }); + + it('Displays a default dismiss form', () => { + const { getByText } = render( + , + ); + expect(getByText('Dismiss this action item?')).toBeInTheDocument(); + expect( + getByText('This action item will be dismissed for all of Ramones.'), + ).toBeInTheDocument(); + AlertDismissOptions.forEach(a => + expect(getByText(a.label)).toBeInTheDocument(), + ); + }); + + it('Displays a custom dismiss form', () => { + const { getByText } = render( + , + ); + expect(getByText('Dismiss this action item?')).toBeInTheDocument(); + expect(getByText('You. Complete. Me.')).toBeInTheDocument(); + expect( + getByText('This action item will be dismissed for all of Ramones.'), + ).toBeInTheDocument(); + }); + + it('Displays a custom accept form', () => { + const { getByText } = render( + , + ); + expect(getByText('Accept this action item?')).toBeInTheDocument(); + expect(getByText('You. Complete. Me.')).toBeInTheDocument(); + expect( + getByText('This action item will be accepted for all of Ramones.'), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertDialog.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertDialog.tsx new file mode 100644 index 0000000000..aa5b8c59fb --- /dev/null +++ b/plugins/cost-insights/src/components/AlertInsights/AlertDialog.tsx @@ -0,0 +1,210 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useEffect, useRef, useState } from 'react'; +import { default as CloseIcon } from '@material-ui/icons/Close'; +import { + Box, + Button, + Divider, + Dialog, + DialogActions, + IconButton, + DialogContent, + Typography, +} from '@material-ui/core'; +import { + AlertAcceptForm, + AlertDismissForm, + AlertSnoozeForm, +} from '../../forms'; +import { useAlertDialogStyles as useStyles } from '../../utils/styles'; +import { choose } from '../../utils/alerts'; +import { Alert, Maybe } from '../../types'; + +const DEFAULT_FORM_ID = 'alert-form'; + +type AlertDialogProps = { + open: boolean; + group: string; + snoozed: Maybe; + accepted: Maybe; + dismissed: Maybe; + onClose: () => void; + onSubmit: (data: any) => void; +}; + +export const AlertDialog = ({ + open, + group, + snoozed, + accepted, + dismissed, + onClose, + onSubmit, +}: AlertDialogProps) => { + const classes = useStyles(); + const [isButtonDisabled, setDisabled] = useState(true); + const acceptRef = useRef>(null); + const snoozeRef = useRef>(null); + const dismissRef = useRef>(null); + + useEffect(() => { + if (open) { + setDisabled(true); + } else { + setDisabled(false); + } + }, [open]); + + function disableSubmit(isDisabled: boolean) { + setDisabled(isDisabled); + } + + function onDialogClose() { + onClose(); + setDisabled(true); + } + + const SnoozeForm = snoozed?.SnoozeForm ?? AlertSnoozeForm; + const AcceptForm = accepted?.AcceptForm ?? AlertAcceptForm; + const DismissForm = dismissed?.DismissForm ?? AlertDismissForm; + + const isSnoozeFormDisplayed = !!snoozed?.onSnoozed; + const isAcceptFormDisplayed = !!accepted?.onAccepted; + const isDismissFormDisplayed = !!dismissed?.onDismissed; + + const status = [ + isAcceptFormDisplayed, + isSnoozeFormDisplayed, + isDismissFormDisplayed, + ] as const; + + const TransitionProps = { + mountOnEnter: true, + unmountOnExit: true, + // Wait for child component to mount; avoid recycling refs. + onEntered() { + if (acceptRef.current) { + acceptRef.current.id = DEFAULT_FORM_ID; + } + if (snoozeRef.current) { + snoozeRef.current.id = DEFAULT_FORM_ID; + } + if (dismissRef.current) { + dismissRef.current.id = DEFAULT_FORM_ID; + } + }, + }; + + return ( + + + + + + + + + + + {choose(status, ['Accept', 'Snooze', 'Dismiss'])} this action + item? + + + + + This action item will be{' '} + {choose(status, ['accepted', 'snoozed', 'dismissed'])} for all of{' '} + {group}. + + + + + + + {choose(status, [ + accepted?.title, + snoozed?.title, + dismissed?.title, + ])} + + + + {choose(status, [ + accepted?.subtitle, + snoozed?.subtitle, + dismissed?.subtitle, + ])} + + + {isSnoozeFormDisplayed && ( + + )} + {isDismissFormDisplayed && ( + + )} + {isAcceptFormDisplayed && ( + + )} + + + + + + + ); +}; diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertInsights.test.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertInsights.test.tsx new file mode 100644 index 0000000000..cd8444cb1f --- /dev/null +++ b/plugins/cost-insights/src/components/AlertInsights/AlertInsights.test.tsx @@ -0,0 +1,89 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; +import { AlertInsights } from './AlertInsights'; +import { + MockScrollProvider, + MockAlertsProvider, + MockLoadingProvider, +} from '../../utils/tests'; + +function renderInContext(children: JSX.Element) { + return render( + + + {children} + + , + ); +} + +describe('', () => { + it('should display the correct header if there are active action items', () => { + const { getByText, queryByText } = renderInContext( + , + ); + expect( + getByText( + 'This section outlines suggested action items your team can address to improve cloud costs.', + ), + ).toBeInTheDocument(); + expect(queryByText('Hidden Action Item')).not.toBeInTheDocument(); + }); + + it('should display alert summary if there are hidden action items', async () => { + const { getByText, getByRole } = renderInContext( + , + ); + + expect( + getByText( + "All of your team's action items are hidden. Maybe it's time to give them another look?", + ), + ).toBeInTheDocument(); + expect(getByText('Hidden Action Item')).toBeInTheDocument(); + + fireEvent.click(getByRole('button', { name: 'expand' })); + await waitFor(() => getByRole('img', { name: 'snoozed' })); + + expect(getByText('Vol. 4')).toBeInTheDocument(); + expect(getByText('Sabotage')).toBeInTheDocument(); + }); +}); diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx index 4606906f04..1bcb684052 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx @@ -14,31 +14,203 @@ * limitations under the License. */ -import React from 'react'; -import { Grid } from '@material-ui/core'; -import { AlertInsightsSection } from './AlertInsightsSection'; +import React, { useCallback, useEffect, useState } from 'react'; +import pluralize from 'pluralize'; +import { Box, Grid, Snackbar } from '@material-ui/core'; +import { default as MuiAlert } from '@material-ui/lab/Alert'; +import { AlertDialog } from './AlertDialog'; +import { AlertStatusSummary } from './AlertStatusSummary'; +import { AlertStatusSummaryButton } from './AlertStatusSummaryButton'; import { AlertInsightsHeader } from './AlertInsightsHeader'; -import { Alert } from '../../types'; +import { AlertInsightsSection } from './AlertInsightsSection'; +import { + useAlerts, + useScroll, + useLoading, + ScrollType, + MapLoadingToProps, +} from '../../hooks'; +import { DefaultLoadingAction } from '../../utils/loading'; +import { Alert, AlertOptions, Maybe } from '../../types'; +import { sumOfAllAlerts } from '../../utils/alerts'; -const title = "Your team's action items"; -const subtitle = - 'This section outlines suggested action items your team can address to improve cloud costs.'; +type MapLoadingtoAlerts = (isLoading: boolean) => void; + +const mapLoadingToAlerts: MapLoadingToProps = ({ + dispatch, +}) => (isLoading: boolean) => + dispatch({ [DefaultLoadingAction.CostInsightsAlerts]: isLoading }); type AlertInsightsProps = { - alerts: Array; + group: string; + active: Alert[]; + snoozed: Alert[]; + accepted: Alert[]; + dismissed: Alert[]; }; -export const AlertInsights = ({ alerts }: AlertInsightsProps) => ( - - - - - - {alerts.map((alert, index) => ( - - +export const AlertInsights = ({ + group, + active, + snoozed, + accepted, + dismissed, +}: AlertInsightsProps) => { + const [alerts, setAlerts] = useAlerts(); + const [scroll, , ScrollAnchor] = useScroll(); + const dispatchLoadingAlerts = useLoading(mapLoadingToAlerts); + // Allow users to pass null values for data. + const [data, setData] = useState>(undefined); + const [error, setError] = useState>(null); + const [isDialogOpen, setDialogOpen] = useState(false); + const [isSummaryOpen, setSummaryOpen] = useState(false); + const [isSnackbarOpen, setSnackbarOpen] = useState(false); + + const closeDialog = useCallback(() => { + setData(undefined); + setDialogOpen(false); + setAlerts({ dismissed: null, snoozed: null, accepted: null }); + }, [setAlerts]); + + useEffect(() => { + async function callHandler( + options: AlertOptions, + callback: (options: AlertOptions) => Promise, + ) { + closeDialog(); + dispatchLoadingAlerts(true); + try { + const a: Alert[] = await callback(options); + setAlerts({ alerts: a }); + } catch (e) { + setError(e); + } finally { + dispatchLoadingAlerts(false); + } + } + + const options: AlertOptions = { data, group }; + const onSnoozed = alerts.snoozed?.onSnoozed?.bind(alerts.snoozed) ?? null; + const onAccepted = + alerts.accepted?.onAccepted?.bind(alerts.accepted) ?? null; + const onDismissed = + alerts.dismissed?.onDismissed?.bind(alerts.dismissed) ?? null; + + if (data !== undefined) { + if (onSnoozed) { + callHandler(options, onSnoozed); + } else if (onAccepted) { + callHandler(options, onAccepted); + } else if (onDismissed) { + callHandler(options, onDismissed); + } + } + }, [group, data, alerts, setAlerts, closeDialog, dispatchLoadingAlerts]); + + useEffect(() => { + if (scroll === ScrollType.AlertSummary) { + setSummaryOpen(true); + } + }, [scroll]); + + useEffect(() => { + if (error) { + setSnackbarOpen(true); + } else { + setSnackbarOpen(false); + } + }, [error]); + + useEffect(() => { + function toggleDialogOnStatusChange() { + const isAlertSnoozed = !!alerts.snoozed; + const isAlertAccepted = !!alerts.accepted; + const isAlertDismissed = !!alerts.dismissed; + + if (isAlertSnoozed || isAlertDismissed || isAlertAccepted) { + setDialogOpen(true); + } else { + setDialogOpen(false); + } + } + + toggleDialogOnStatusChange(); + }, [alerts.snoozed, alerts.dismissed, alerts.accepted]); + + function onSnackbarClose() { + setError(null); + } + + function onDialogSubmit(data: any) { + setData(data); + } + + function onSummaryButtonClick() { + setSummaryOpen(prevOpen => !prevOpen); + } + + const total = [accepted, snoozed, dismissed].reduce(sumOfAllAlerts, 0); + + const isAlertStatusSummaryDisplayed = !!total; + const isAlertInsightSectionDisplayed = !!active.length; + // AlertInsights will not display if there aren't any active or hidden items. + + return ( + + + + + {isAlertInsightSectionDisplayed && ( + + {active.map((alert, index) => ( + + + + ))} - ))} + )} + {isAlertStatusSummaryDisplayed && ( + + + + + {pluralize('Hidden Action Item', total)} + + + + + )} + + + + {error?.message} + + - -); + ); +}; diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsHeader.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsHeader.tsx index b58f6973ae..a8006b81d0 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsHeader.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsHeader.tsx @@ -30,10 +30,11 @@ export const AlertInsightsHeader = ({ subtitle, }: AlertInsightsHeaderProps) => { const classes = useStyles(); - const { ScrollAnchor } = useScroll(DefaultNavigation.AlertInsightsHeader); + const [, , ScrollAnchor] = useScroll(); + return ( - + {title}{' '} diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.test.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.test.tsx index 54d350298c..f04e1d8b01 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.test.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.test.tsx @@ -17,10 +17,10 @@ import React from 'react'; import { AlertInsightsSection } from './AlertInsightsSection'; import { render } from '@testing-library/react'; import { Alert } from '../../types'; -import { MockScrollProvider } from '../..'; +import { AlertState } from '../../hooks'; +import { MockScrollProvider, MockAlertsProvider } from '../../utils/tests'; const mockAlert: Alert = { - element:
, subtitle: 'Wherefore was I to this keen mockery born? When at your hands did I deserve this scorn?', title: 'Mock alert', @@ -29,26 +29,110 @@ const mockAlert: Alert = { describe('', () => { it('Renders alert without exploding', () => { - const { getByText } = render( - - - , + const { getByText, queryByText } = render( + + + + + , ); expect(getByText(mockAlert.title)).toBeInTheDocument(); expect(getByText(mockAlert.subtitle)).toBeInTheDocument(); expect(getByText('View Instructions')).toBeInTheDocument(); + expect(queryByText('Snooze')).not.toBeInTheDocument(); + expect(queryByText('Accept')).not.toBeInTheDocument(); + expect(queryByText('Dismiss')).not.toBeInTheDocument(); }); it('Hides instructions button if url is not provided', () => { - const alert = { + const alert: Alert = { ...mockAlert, url: undefined, }; const { queryByText } = render( - - - , + + + + + , ); expect(queryByText('View Instructions')).not.toBeInTheDocument(); }); + + it('Displays a snooze button if a hook is provided', () => { + const alert: Alert = { + ...mockAlert, + onSnoozed: jest.fn(), + }; + + const context: AlertState = { + alerts: [], + snoozed: alert, + dismissed: null, + accepted: null, + }; + + const { queryByText, getByText } = render( + + + + + , + ); + + expect(getByText('Snooze')).toBeInTheDocument(); + expect(queryByText('Accept')).not.toBeInTheDocument(); + expect(queryByText('Dismiss')).not.toBeInTheDocument(); + }); + + it('Displays a dismiss button if a hook is provided', () => { + const alert: Alert = { + ...mockAlert, + onDismissed: jest.fn(), + }; + const context: AlertState = { + alerts: [], + snoozed: null, + dismissed: alert, + accepted: null, + }; + + const { queryByText, getByText } = render( + + + + + , + ); + + expect(getByText('Dismiss')).toBeInTheDocument(); + expect(queryByText('Accept')).not.toBeInTheDocument(); + expect(queryByText('Snooze')).not.toBeInTheDocument(); + }); + + it('Displays an accept button if a hook is provided', () => { + const alert: Alert = { + ...mockAlert, + onAccepted: jest.fn(), + }; + + const context: AlertState = { + alerts: [], + snoozed: null, + dismissed: null, + accepted: alert, + }; + + const { queryByText, getByText } = render( + + + + + , + ); + + expect(getByText('Accept')).toBeInTheDocument(); + expect(queryByText('Snooze')).not.toBeInTheDocument(); + expect(queryByText('Dismiss')).not.toBeInTheDocument(); + }); }); diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.tsx index 7e371c8001..dda2d66f3f 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.tsx @@ -15,7 +15,11 @@ */ import React from 'react'; import { Box, Button } from '@material-ui/core'; +import { default as SnoozeIcon } from '@material-ui/icons/AccessTime'; +import { default as AcceptIcon } from '@material-ui/icons/Check'; +import { default as DismissIcon } from '@material-ui/icons/Delete'; import { AlertInsightsSectionHeader } from './AlertInsightsSectionHeader'; +import { useAlerts } from '../../hooks'; import { Alert } from '../../types'; type AlertInsightsSectionProps = { @@ -27,18 +31,60 @@ export const AlertInsightsSection = ({ alert, number, }: AlertInsightsSectionProps) => { + const [, setAlerts] = useAlerts(); + + const isSnoozeButtonDisplayed = !!alert.onSnoozed; + const isAcceptButtonDisplayed = !!alert.onAccepted; + const isDismissButtonDisplayed = !!alert.onDismissed; + const isButtonGroupDisplayed = + isSnoozeButtonDisplayed || + isAcceptButtonDisplayed || + isDismissButtonDisplayed; + return ( - - - {alert.url && ( - - + + + {isButtonGroupDisplayed && ( + + {isAcceptButtonDisplayed && ( + + + + )} + {isSnoozeButtonDisplayed && ( + + + + )} + {isDismissButtonDisplayed && ( + + )} )} {alert.element} diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSectionHeader.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSectionHeader.tsx index 3613e3adc8..c463a05609 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSectionHeader.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSectionHeader.tsx @@ -15,34 +15,47 @@ */ import React from 'react'; -import { Avatar, Box, Grid, Typography } from '@material-ui/core'; +import { Avatar, Box, Button, Grid, Typography } from '@material-ui/core'; import { useAlertInsightsSectionStyles as useStyles } from '../../utils/styles'; import { useScroll } from '../../hooks'; +import { Alert } from '../../types'; type AlertInsightsSectionHeaderProps = { + alert: Alert; number: number; - title: string; - subtitle: string; }; export const AlertInsightsSectionHeader = ({ + alert, number, - title, - subtitle, }: AlertInsightsSectionHeaderProps) => { - const { ScrollAnchor } = useScroll(`alert-${number}`); + const [, , ScrollAnchor] = useScroll(); const classes = useStyles(); + + const isViewInstructionsButtonDisplayed = !!alert.url; + return ( - - + + - {number} - - - {title} - {subtitle} + + + {number} + + + {alert.title} + {alert.subtitle} + + + {isViewInstructionsButtonDisplayed && ( + + + + )} ); diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertStatusSummary.test.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertStatusSummary.test.tsx new file mode 100644 index 0000000000..830db19fcd --- /dev/null +++ b/plugins/cost-insights/src/components/AlertInsights/AlertStatusSummary.test.tsx @@ -0,0 +1,58 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { AlertStatusSummary } from './AlertStatusSummary'; +import { render } from '@testing-library/react'; +import { Alert, AlertStatus } from '../../types'; +import { MockScrollProvider } from '../../utils/tests'; + +const mockSnoozed: Alert = { + title: 'snoozed-title', + subtitle: 'snoozed-subtitle', + status: AlertStatus.Snoozed, +}; + +const mockAccepted: Alert = { + title: 'accepted-title', + subtitle: 'accepted-subtitle', + status: AlertStatus.Accepted, +}; + +const mockDismissed: Alert = { + title: 'dismissed-title', + subtitle: 'dismissed-subtitle', + status: AlertStatus.Dismissed, +}; + +describe('', () => { + it('should display alerts', () => { + const { getByText, getByRole } = render( + + + , + ); + [mockSnoozed, mockAccepted, mockDismissed].forEach(a => { + expect(getByText(a.title)).toBeInTheDocument(); + expect(getByText(a.subtitle)).toBeInTheDocument(); + expect(getByRole('img', { name: a.status })).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertStatusSummary.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertStatusSummary.tsx new file mode 100644 index 0000000000..cb0a149511 --- /dev/null +++ b/plugins/cost-insights/src/components/AlertInsights/AlertStatusSummary.tsx @@ -0,0 +1,116 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Fragment } from 'react'; +import { Avatar, Box, Collapse, Divider } from '@material-ui/core'; +import { default as AcceptIcon } from '@material-ui/icons/Check'; +import { default as DismissIcon } from '@material-ui/icons/Delete'; +import { default as SnoozeIcon } from '@material-ui/icons/AccessTime'; +import { ActionItemCard } from '../ActionItems'; +import { Alert, AlertStatus } from '../../types'; +import { useActionItemCardStyles as useStyles } from '../../utils/styles'; + +type AlertStatusSummaryProps = { + open: boolean; + snoozed: Alert[]; + accepted: Alert[]; + dismissed: Alert[]; +}; + +export const AlertStatusSummary = ({ + open, + snoozed, + accepted, + dismissed, +}: AlertStatusSummaryProps) => { + const classes = useStyles(); + + const isSnoozedListDisplayed = !!snoozed.length; + const isAcceptedListDisplayed = !!accepted.length; + const isDismissedListDisplayed = !!dismissed.length; + + return ( + + {isAcceptedListDisplayed && ( + + {accepted.map((alert, index) => ( + + + {/* Icons indicate alert status. Do not hide from accesibility tree */} + + + } + /> + {index < accepted.length - 1 && } + + ))} + + )} + {isSnoozedListDisplayed && ( + + {snoozed.map((alert, index) => ( + + + + + } + /> + {index < snoozed.length - 1 && } + + ))} + + )} + {isDismissedListDisplayed && ( + + {dismissed.map((alert, index) => ( + + + + + } + /> + {index < dismissed.length - 1 && } + + ))} + + )} + + ); +}; diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertStatusSummaryButton.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertStatusSummaryButton.tsx new file mode 100644 index 0000000000..ed8c7fabbb --- /dev/null +++ b/plugins/cost-insights/src/components/AlertInsights/AlertStatusSummaryButton.tsx @@ -0,0 +1,54 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState, MouseEventHandler, PropsWithChildren } from 'react'; +import classnames from 'classnames'; +import { Button } from '@material-ui/core'; +import { default as ExpandMoreIcon } from '@material-ui/icons/ExpandMore'; +import { useAlertStatusSummaryButtonStyles as useStyles } from '../../utils/styles'; + +type AlertStatusSummaryButtonProps = { + onClick: MouseEventHandler; +}; + +export const AlertStatusSummaryButton = ({ + children, + onClick, +}: PropsWithChildren) => { + const classes = useStyles(); + const [clicked, setClicked] = useState(false); + const iconClassName = classnames(classes.icon, { + [classes.clicked]: clicked, + }); + + const handleOnClick: MouseEventHandler = e => { + setClicked(prevClicked => !prevClicked); + onClick(e); + }; + + return ( + + ); +}; diff --git a/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.tsx b/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.tsx index a30b6c5a90..dd1652af46 100644 --- a/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.tsx +++ b/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.tsx @@ -105,13 +105,14 @@ export const CostInsightsNavigation = React.memo( const NavigationMenuItem = ({ navigation, icon, title }: NavigationItem) => { const classes = useStyles(); - const { scrollIntoView } = useScroll(navigation); + const [, setScroll] = useScroll(); + return ( setScroll(navigation)} > {icon} { const classes = useSubtleTypographyStyles(); @@ -54,16 +68,24 @@ export const CostInsightsPage = () => { const config = useConfig(); const groups = useGroups(); const lastCompleteBillingDate = useLastCompleteBillingDate(); + const [alerts, setAlerts] = useAlerts(); const [currency, setCurrency] = useCurrency(); const [projects, setProjects] = useState>(null); const [products, setProducts] = useState>(null); const [dailyCost, setDailyCost] = useState>(null); const [metricData, setMetricData] = useState>(null); - const [alerts, setAlerts] = useState>(null); const [error, setError] = useState>(null); const { pageFilters, setPageFilters } = useFilters(p => p); + const snoozed = useMemo(() => alerts.alerts.filter(isSnoozed), [alerts]); + const accepted = useMemo(() => alerts.alerts.filter(isAccepted), [alerts]); + const dismissed = useMemo(() => alerts.alerts.filter(isDismissed), [alerts]); + const activeAlerts = useMemo(() => alerts.alerts.filter(isActive), [alerts]); + + const isActionItemsDisplayed = !!activeAlerts.length; + const isAlertInsightsDisplayed = !!alerts.alerts.length; + const { loadingActions, loadingGroups, @@ -120,7 +142,7 @@ export const CostInsightsPage = () => { : client.getGroupDailyCost(pageFilters.group, intervals), ]); setProjects(fetchedProjects); - setAlerts(fetchedAlerts); + setAlerts({ alerts: fetchedAlerts }); setMetricData(fetchedMetricData); setDailyCost(fetchedDailyCost); } else { @@ -145,6 +167,7 @@ export const CostInsightsPage = () => { loadingActions, loadingGroups, loadingBillingDate, + setAlerts, dispatchLoadingInsights, dispatchLoadingInitial, dispatchLoadingNone, @@ -177,8 +200,8 @@ export const CostInsightsPage = () => { ); } - // These should be defined, alerts can be an empty array but that's truthy - if (!dailyCost || !alerts) { + + if (!dailyCost) { return ( {`Error: Could not fetch cost insights data for team ${pageFilters.group}`} ); @@ -228,7 +251,7 @@ export const CostInsightsPage = () => { @@ -249,19 +272,22 @@ export const CostInsightsPage = () => { owner={pageFilters.group} groups={groups} hasCostData={!!dailyCost.aggregation.length} - alerts={alerts.length} + alerts={activeAlerts.length} /> - {!!alerts.length && ( - <> - - - - - - - - )} + + + + + + + + @@ -276,14 +302,20 @@ export const CostInsightsPage = () => { - - {!!alerts?.length && ( + + - + - )} - - {!alerts.length && } + + + {!isAlertInsightsDisplayed && } ( - + + + diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx index 39adc4bbf1..1539e431fd 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx @@ -49,7 +49,7 @@ export const CostOverviewCard = ({ const config = useConfig(); const [tabIndex, setTabIndex] = useState(0); - const { ScrollAnchor } = useScroll(DefaultNavigation.CostOverviewCard); + const [, , ScrollAnchor] = useScroll(); const { setDuration, setProject, setMetric, ...filters } = useFilters( mapFiltersToProps, ); @@ -95,7 +95,7 @@ export const CostOverviewCard = ({ return ( - + {dailyCostData.groupedCosts && } diff --git a/plugins/cost-insights/src/components/MigrationAlertCard/MigrationAlertCard.tsx b/plugins/cost-insights/src/components/MigrationAlertCard/MigrationAlertCard.tsx new file mode 100644 index 0000000000..f4d174d521 --- /dev/null +++ b/plugins/cost-insights/src/components/MigrationAlertCard/MigrationAlertCard.tsx @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Box } from '@material-ui/core'; +import { InfoCard } from '@backstage/core'; +import { MigrationBarChartLegend } from './MigrationBarChartLegend'; +import { MigrationBarChart } from './MigrationBarChart'; +import { MigrationData } from '../../types'; + +type MigrationAlertProps = { + data: MigrationData; + title: string; + subheader: string; + currentProduct: string; + comparedProduct: string; +}; + +export const MigrationAlertCard = ({ + data, + title, + subheader, + currentProduct, + comparedProduct, +}: MigrationAlertProps) => { + return ( + + + + + + + + + + + ); +}; diff --git a/plugins/cost-insights/src/components/MigrationAlertCard/MigrationBarChart.tsx b/plugins/cost-insights/src/components/MigrationAlertCard/MigrationBarChart.tsx new file mode 100644 index 0000000000..60aa49b2bc --- /dev/null +++ b/plugins/cost-insights/src/components/MigrationAlertCard/MigrationBarChart.tsx @@ -0,0 +1,54 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { BarChart } from '../BarChart'; +import { + BarChartOptions, + CostInsightsTheme, + Entity, + ResourceData, +} from '../../types'; +import { useTheme } from '@material-ui/core'; + +type MigrationBarChartProps = { + currentProduct: string; + comparedProduct: string; + services: Array; +}; + +export const MigrationBarChart = ({ + currentProduct, + comparedProduct, + services, +}: MigrationBarChartProps) => { + const theme = useTheme(); + + const options: BarChartOptions = { + previousFill: theme.palette.magenta, + currentFill: theme.palette.yellow, + previousName: comparedProduct, + currentName: currentProduct, + }; + + const resources: ResourceData[] = services.map(service => ({ + name: service.id, + previous: service.aggregation[0], + current: service.aggregation[1], + })); + + return ; +}; diff --git a/plugins/cost-insights/src/components/MigrationAlertCard/MigrationBarChartLegend.tsx b/plugins/cost-insights/src/components/MigrationAlertCard/MigrationBarChartLegend.tsx new file mode 100644 index 0000000000..a84e865e28 --- /dev/null +++ b/plugins/cost-insights/src/components/MigrationAlertCard/MigrationBarChartLegend.tsx @@ -0,0 +1,58 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Box, useTheme } from '@material-ui/core'; +import { CostGrowth } from '../CostGrowth'; +import { LegendItem } from '../LegendItem'; +import { ChangeStatistic, CostInsightsTheme, Duration } from '../../types'; +import { monthOf } from '../../utils/formatters'; + +export type MigrationBarChartLegendProps = { + change: ChangeStatistic; + startDate: string; + currentProduct: string; + comparedProduct: string; +}; + +export const MigrationBarChartLegend = ({ + currentProduct, + comparedProduct, + change, + startDate, +}: MigrationBarChartLegendProps) => { + const theme = useTheme(); + return ( + + + + {currentProduct} + + + + + {comparedProduct} + + + + + + + ); +}; diff --git a/plugins/cost-insights/src/components/AlertActionCardList/index.ts b/plugins/cost-insights/src/components/MigrationAlertCard/index.ts similarity index 90% rename from plugins/cost-insights/src/components/AlertActionCardList/index.ts rename to plugins/cost-insights/src/components/MigrationAlertCard/index.ts index a0ce118482..4feb749c27 100644 --- a/plugins/cost-insights/src/components/AlertActionCardList/index.ts +++ b/plugins/cost-insights/src/components/MigrationAlertCard/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { AlertActionCardList } from './AlertActionCardList'; +export { MigrationAlertCard } from './MigrationAlertCard'; diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx index fc37bc5dd6..406affcd15 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx @@ -77,17 +77,6 @@ const renderProductInsightsCardInTestApp = async ( ); describe('', () => { - it('Renders the scroll anchors', async () => { - const rendered = await renderProductInsightsCardInTestApp( - mockProductCost, - MockComputeEngine, - Duration.P30D, - ); - expect( - rendered.queryByTestId(`scroll-test-compute-engine`), - ).toBeInTheDocument(); - }); - it('Should render the right subheader for products with cost data', async () => { const entity = { ...mockProductCost, diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx index 534ef490c3..828d229c94 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx @@ -60,7 +60,7 @@ export const ProductInsightsCard = ({ }: PropsWithChildren) => { const classes = useStyles(); const mountedRef = useRef(false); - const { ScrollAnchor } = useScroll(product.kind); + const [, , ScrollAnchor] = useScroll(); const [error, setError] = useState>(null); const dispatchLoading = useLoading(mapLoadingToProps); const lastCompleteBillingDate = useLastCompleteBillingDate(); @@ -107,7 +107,7 @@ export const ProductInsightsCard = ({ if (error || !entity) { return ( - + {error ? error.message @@ -123,7 +123,7 @@ export const ProductInsightsCard = ({ subheader={subheader} headerProps={headerProps} > - + {entities.length ? ( ; + +export const AlertAcceptForm = forwardRef< + HTMLFormElement, + AlertAcceptFormProps +>(({ onSubmit, disableSubmit }, ref) => { + const [checked, setChecked] = useState(false); + + const onFormSubmit: FormEventHandler = e => { + e.preventDefault(); + onSubmit(null); + }; + + const onChecked = (_: ChangeEvent, isChecked: boolean) => { + setChecked(isChecked); + disableSubmit(!isChecked); + }; + + return ( +
+ + + } + /> + +
+ ); +}); diff --git a/plugins/cost-insights/src/forms/AlertDismissForm.tsx b/plugins/cost-insights/src/forms/AlertDismissForm.tsx new file mode 100644 index 0000000000..1b3cbdb363 --- /dev/null +++ b/plugins/cost-insights/src/forms/AlertDismissForm.tsx @@ -0,0 +1,156 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { + ChangeEvent, + useEffect, + useState, + forwardRef, + FormEventHandler, +} from 'react'; +import { + Box, + Collapse, + FormControl, + FormControlLabel, + TextField, + Typography, + Radio, + RadioGroup, +} from '@material-ui/core'; +import { + Alert, + AlertFormProps, + AlertDismissReason, + AlertDismissOptions, + AlertDismissFormData, + Maybe, +} from '../types'; +import { useAlertDialogStyles as useStyles } from '../utils/styles'; + +export type AlertDismissFormProps = AlertFormProps; + +export const AlertDismissForm = forwardRef< + HTMLFormElement, + AlertDismissFormProps +>(({ onSubmit, disableSubmit }, ref) => { + const classes = useStyles(); + const [other, setOther] = useState>(null); + const [feedback, setFeedback] = useState>(null); + const [reason, setReason] = useState( + AlertDismissReason.Resolved, + ); + + const onFormSubmit: FormEventHandler = e => { + function submit() { + onSubmit({ + other: other, + reason: reason, + feedback: feedback, + }); + } + + e.preventDefault(); + if (reason) { + submit(); + } + }; + + const onReasonChange = (_: ChangeEvent, value: string) => { + setReason(value as AlertDismissReason); + }; + + const onOtherChange = (e: ChangeEvent) => { + return e.target.value + ? setOther(e.target.value as AlertDismissReason) + : setOther(null); + }; + + const onFeedbackChange = (e: ChangeEvent) => { + return e.target.value + ? setFeedback(e.target.value as AlertDismissReason) + : setFeedback(null); + }; + + useEffect(() => { + function validateDismissForm() { + if (reason === AlertDismissReason.Other) { + if (other) { + disableSubmit(false); + } else { + disableSubmit(true); + } + } else if (reason) { + disableSubmit(false); + } else { + disableSubmit(true); + } + } + + validateDismissForm(); + }, [reason, other, disableSubmit]); + + return ( +
+ + + Reason for dismissing? + + + + {AlertDismissOptions.map(option => ( + } + /> + ))} + + + + + + + + + Any other feedback you can provide? + + + +
+ ); +}); diff --git a/plugins/cost-insights/src/forms/AlertSnoozeForm.tsx b/plugins/cost-insights/src/forms/AlertSnoozeForm.tsx new file mode 100644 index 0000000000..51218fe844 --- /dev/null +++ b/plugins/cost-insights/src/forms/AlertSnoozeForm.tsx @@ -0,0 +1,115 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { + ChangeEvent, + useEffect, + useState, + forwardRef, + FormEventHandler, +} from 'react'; +import dayjs from 'dayjs'; +import { + Box, + Collapse, + FormControl, + FormControlLabel, + RadioGroup, + Radio, + Typography, +} from '@material-ui/core'; +import { + Alert, + AlertFormProps, + Duration, + DEFAULT_DATE_FORMAT, + Maybe, + AlertSnoozeFormData, + AlertSnoozeOptions, +} from '../types'; +import { useAlertDialogStyles as useStyles } from '../utils/styles'; +import { intervalsOf } from '../utils/duration'; + +export type AlertSnoozeFormProps = AlertFormProps; + +export const AlertSnoozeForm = forwardRef< + HTMLFormElement, + AlertSnoozeFormProps +>(({ onSubmit, disableSubmit }, ref) => { + const classes = useStyles(); + const [error, setError] = useState>(null); + const [duration, setDuration] = useState>(Duration.P7D); + + const onFormSubmit: FormEventHandler = e => { + e.preventDefault(); + if (duration) { + const repeatInterval = 1; + const inclusiveEndDate = dayjs().format(DEFAULT_DATE_FORMAT); + onSubmit({ + intervals: intervalsOf(duration, inclusiveEndDate, repeatInterval), + }); + } else { + setError(new Error('Please select an option.')); + } + }; + + const onSnoozeDurationChange = ( + _: ChangeEvent, + value: string, + ) => { + setDuration(value as Duration); + }; + + useEffect(() => { + function clearErrorOnFormDataChange() { + disableSubmit(false); + setError(prevError => (prevError ? null : prevError)); + } + + clearErrorOnFormDataChange(); + }, [duration, disableSubmit]); + + const isErrorMessageDisplayed = !!error; + + return ( +
+ + + For how long? + + + {error?.message} + + + + {AlertSnoozeOptions.map(option => ( + } + /> + ))} + + + +
+ ); +}); diff --git a/plugins/cost-insights/src/forms/MigrationDismissForm.tsx b/plugins/cost-insights/src/forms/MigrationDismissForm.tsx new file mode 100644 index 0000000000..ce71af3a37 --- /dev/null +++ b/plugins/cost-insights/src/forms/MigrationDismissForm.tsx @@ -0,0 +1,108 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { + useEffect, + useState, + forwardRef, + FormEventHandler, + ChangeEvent, +} from 'react'; +import { + Checkbox, + FormControl, + FormControlLabel, + FormGroup, + Typography, +} from '@material-ui/core'; +import { AlertFormProps, Entity } from '../types'; +import { MigrationAlert } from '../alerts'; +import { findAlways } from '../utils/assert'; + +export type MigrationDismissFormData = { + services: Entity[]; +}; + +export type MigrationDismissFormProps = AlertFormProps< + MigrationAlert, + MigrationDismissFormData +>; + +export const MigrationDismissForm = forwardRef< + HTMLFormElement, + MigrationDismissFormProps +>(({ onSubmit, disableSubmit, alert }, ref) => { + const [services, setServices] = useState(alert.data.services); + + const onFormSubmit: FormEventHandler = e => { + /* Remember to prevent default form behavior */ + e.preventDefault(); + onSubmit({ services: services }); + }; + + const onCheckboxChange = ( + e: ChangeEvent, + checked: boolean, + ) => { + if (checked) { + const service = findAlways( + alert.data.services, + s => s.id === e.target.value, + ); + setServices(prevServices => prevServices.concat(service)); + } else { + setServices(prevServices => + prevServices.filter(p => p.id !== e.target.value), + ); + } + }; + + /* Submit button is disabled by default. Use props.disableSubmit to toggle disabled state. */ + useEffect(() => { + if (services.length) { + disableSubmit(false); + } else { + disableSubmit(true); + } + }, [services, disableSubmit]); + + return ( + /* All custom forms must accept a ref and implement an onSubmit handler. */ +
+ + + Or choose which services to dismiss this alert for. + + + {alert.data.services.map((service, index) => ( + p.id === service.id)} + onChange={onCheckboxChange} + /> + } + /> + ))} + + +
+ ); +}); diff --git a/plugins/cost-insights/src/forms/index.ts b/plugins/cost-insights/src/forms/index.ts new file mode 100644 index 0000000000..5b8384a293 --- /dev/null +++ b/plugins/cost-insights/src/forms/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { AlertAcceptForm } from './AlertAcceptForm'; +export { AlertSnoozeForm } from './AlertSnoozeForm'; +export { AlertDismissForm } from './AlertDismissForm'; +export { MigrationDismissForm } from './MigrationDismissForm'; +export type { MigrationDismissFormData } from './MigrationDismissForm'; diff --git a/plugins/cost-insights/src/hooks/index.ts b/plugins/cost-insights/src/hooks/index.ts index 94c556763c..c1a4b3512c 100644 --- a/plugins/cost-insights/src/hooks/index.ts +++ b/plugins/cost-insights/src/hooks/index.ts @@ -16,6 +16,7 @@ export * from './useConfig'; export * from './useCurrency'; +export * from './useAlerts'; export * from './useFilters'; export * from './useCurrency'; export * from './useGroups'; diff --git a/plugins/cost-insights/src/hooks/useAlerts.tsx b/plugins/cost-insights/src/hooks/useAlerts.tsx new file mode 100644 index 0000000000..6bc311b09b --- /dev/null +++ b/plugins/cost-insights/src/hooks/useAlerts.tsx @@ -0,0 +1,76 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { + useReducer, + Dispatch, + SetStateAction, + createContext, + useContext, + PropsWithChildren, +} from 'react'; +import { Alert, Maybe } from '../types'; + +export type AlertsContextProps = { + alerts: AlertState; + setAlerts: Dispatch>>; +}; + +export const AlertsContext = createContext( + undefined, +); + +export type AlertState = { + alerts: Alert[]; + snoozed: Maybe; + accepted: Maybe; + dismissed: Maybe; +}; + +const initialState: AlertState = { + alerts: [], + snoozed: null, + accepted: null, + dismissed: null, +}; + +const reducer = ( + prevState: AlertState, + action: SetStateAction>, +): AlertState => ({ + ...prevState, + ...action, +}); + +export const AlertsProvider = ({ children }: PropsWithChildren<{}>) => { + const [alerts, setAlerts] = useReducer(reducer, initialState); + + return ( + + {children} + + ); +}; + +export function useAlerts() { + const context = useContext(AlertsContext); + return context + ? ([context.alerts, context.setAlerts] as const) + : assertNever(); +} + +function assertNever(): never { + throw new Error('useAlerts cannot be used outside AlertsContext provider'); +} diff --git a/plugins/cost-insights/src/hooks/useScroll.tsx b/plugins/cost-insights/src/hooks/useScroll.tsx index 0a1abbdad3..7f310d59b0 100644 --- a/plugins/cost-insights/src/hooks/useScroll.tsx +++ b/plugins/cost-insights/src/hooks/useScroll.tsx @@ -15,6 +15,7 @@ */ import React, { Dispatch, + ElementType, SetStateAction, useState, useContext, @@ -22,19 +23,14 @@ import React, { useRef, PropsWithChildren, } from 'react'; -import { CSSProperties } from '@material-ui/styles'; import { Maybe } from '../types'; export type ScrollTo = Maybe; export type ScrollContextProps = { - scrollTo: ScrollTo; - setScrollTo: Dispatch>; -}; - -export type ScrollUtils = { - ScrollAnchor: (props: Omit) => JSX.Element; - scrollIntoView: () => void; + scroll: ScrollTo; + setScroll: Dispatch>; + ScrollAnchor: ElementType; }; export interface ScrollAnchorProps extends ScrollIntoViewOptions { @@ -49,28 +45,14 @@ export const ScrollContext = React.createContext< export const ScrollAnchor = ({ id, - top, - left, - behavior, block, inline, + left = 0, + top = -20, + behavior = 'smooth', }: ScrollAnchorProps) => { const divRef = useRef(null); - const context = useContext(ScrollContext); - - if (!context) { - assertNever(); - } - - const { scrollTo, setScrollTo } = context; - - const styles: CSSProperties = { - position: 'absolute', - height: 0, - width: 0, - top: top || 0, - left: left || 0, - }; + const [scroll, setScroll] = useScroll(); useEffect(() => { function scrollIntoView() { @@ -80,39 +62,46 @@ export const ScrollAnchor = ({ inline: inline || 'nearest', }; - if (divRef.current && scrollTo === id) { + if (divRef.current && scroll === id) { divRef.current.scrollIntoView(options); - setScrollTo(null); + setScroll(null); } } scrollIntoView(); - }, [scrollTo, setScrollTo, id, behavior, block, inline]); + }, [scroll, setScroll, id, behavior, block, inline]); - return
; + return ( +
+ ); }; export const ScrollProvider = ({ children }: PropsWithChildren<{}>) => { - const [scrollTo, setScrollTo] = useState(null); + const [scroll, setScroll] = useState(null); return ( - + {children} ); }; -export function useScroll(id: ScrollTo): ScrollUtils { +export enum ScrollType { + AlertSummary = 'alert-status-summary', +} + +export function useScroll() { const context = useContext(ScrollContext); if (!context) { assertNever(); } - return { - ScrollAnchor: props => , - scrollIntoView: () => context.setScrollTo(id), - }; + return [context.scroll, context.setScroll, context.ScrollAnchor] as const; } function assertNever(): never { diff --git a/plugins/cost-insights/src/index.ts b/plugins/cost-insights/src/index.ts index 7cd0456ad1..13ce929401 100644 --- a/plugins/cost-insights/src/index.ts +++ b/plugins/cost-insights/src/index.ts @@ -17,8 +17,8 @@ export { plugin } from './plugin'; export * from './client'; export * from './api'; +export { ProjectGrowthAlert, UnlabeledDataflowAlert } from './alerts'; export * from './components'; export { useCurrency } from './hooks'; export * from './types'; export * from './utils/tests'; -export * from './utils/alerts'; diff --git a/plugins/cost-insights/src/types/Alert.ts b/plugins/cost-insights/src/types/Alert.ts index 18b86c8cb8..21e1a779a8 100644 --- a/plugins/cost-insights/src/types/Alert.ts +++ b/plugins/cost-insights/src/types/Alert.ts @@ -13,23 +13,147 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import { ForwardRefExoticComponent, RefAttributes } from 'react'; import { ChangeStatistic } from './ChangeStatistic'; +import { Duration } from './Duration'; import { Maybe } from './Maybe'; /** * Generic alert type with required fields for display. The `element` field will be rendered in * the Cost Insights "Action Items" section. This should use data fetched in the CostInsightsApi * implementation to render an InfoCard or other visualization. + * + * The alert type exposes hooks which can be used to enable and access various events, + * such as when a user dismisses or snoozes an alert. Default forms and buttons + * will be rendered if a hook is defined. + * + * Each default form can be overridden with a custom component. It must be implemented using + * React.forwardRef. See https://reactjs.org/docs/forwarding-refs + * + * Errors thrown within hooks will generate a snackbar error notification. */ + export type Alert = { title: string; subtitle: string; + element?: JSX.Element; + status?: AlertStatus; url?: string; buttonText?: string; // Default: View Instructions - element: JSX.Element; + SnoozeForm?: AlertForm; + AcceptForm?: AlertForm; + DismissForm?: AlertForm; + onSnoozed?(options: AlertOptions): Promise; + onAccepted?(options: AlertOptions): Promise; + onDismissed?(options: AlertOptions): Promise; }; +export type AlertForm< + A extends Alert = any, + Data = any +> = ForwardRefExoticComponent< + AlertFormProps & RefAttributes +>; + +export interface AlertOptions { + data: T; + group: string; +} + +/** + * Default snooze form intervals are expressed using an ISO 8601 repeating interval string. + * For example, R1/P7D/2020-09-02 for 1 week or R1/P30D/2020-09-02 for 1 month. + * + * For example, if a user dismisses an alert on Monday January 01 for 1 week, + * it can be re-served on Monday, January 08. 7 calendar days from January 02, + * inclusive of the last day. + * + * https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals + */ +export interface AlertSnoozeFormData { + intervals: string; +} + +export interface AlertDismissFormData { + other: Maybe; + reason: AlertDismissReason; + feedback: Maybe; +} + +// TODO: Convert enum to literal +export enum AlertStatus { + Snoozed = 'snoozed', + Accepted = 'accepted', + Dismissed = 'dismissed', +} + +export type AlertFormProps = { + alert: A; + onSubmit: (data: FormData) => void; + disableSubmit: (isDisabled: boolean) => void; +}; + +export interface AlertDismissOption { + label: string; + reason: string; +} + +export enum AlertDismissReason { + Other = 'other', + Resolved = 'resolved', + Expected = 'expected', + Seasonal = 'seasonal', + Migration = 'migration', + NotApplicable = 'not-applicable', +} + +export const AlertDismissOptions: AlertDismissOption[] = [ + { + reason: AlertDismissReason.Resolved, + label: 'This action item is now resolved.', + }, + { + reason: AlertDismissReason.Seasonal, + label: 'This is an expected increase at this time of year.', + }, + { + reason: AlertDismissReason.Migration, + label: 'This increase is from a migration in process.', + }, + { + reason: AlertDismissReason.Expected, + label: 'This is an expected increase due to our team’s priorities.', + }, + { + reason: AlertDismissReason.NotApplicable, + label: 'This action item doesn’t make sense for my team.', + }, + { + reason: AlertDismissReason.Other, + label: 'Other (please specify)', + }, +]; + +export type AlertSnoozeOption = { + label: string; + duration: Duration; +}; + +export const AlertSnoozeOptions: AlertSnoozeOption[] = [ + { + duration: Duration.P7D, + label: '1 Week', + }, + { + duration: Duration.P30D, + label: '1 Month', + }, + { + duration: Duration.P3M, + label: '1 Quarter', + }, +]; + export interface AlertCost { id: string; aggregation: [number, number]; diff --git a/plugins/cost-insights/src/types/Duration.ts b/plugins/cost-insights/src/types/Duration.ts index c0f03d5c27..e25f38e63d 100644 --- a/plugins/cost-insights/src/types/Duration.ts +++ b/plugins/cost-insights/src/types/Duration.ts @@ -21,6 +21,7 @@ * September 15. */ export enum Duration { + P7D = 'P7D', P30D = 'P30D', P90D = 'P90D', P3M = 'P3M', diff --git a/plugins/cost-insights/src/utils/alerts.tsx b/plugins/cost-insights/src/utils/alerts.tsx index fbb148f38a..b8dfddf79b 100644 --- a/plugins/cost-insights/src/utils/alerts.tsx +++ b/plugins/cost-insights/src/utils/alerts.tsx @@ -14,50 +14,22 @@ * limitations under the License. */ -import React from 'react'; -import { Alert, UnlabeledDataflowData, ProjectGrowthData } from '../types'; -import { UnlabeledDataflowAlertCard } from '../components/UnlabeledDataflowAlertCard'; -import { ProjectGrowthAlertCard } from '../components/ProjectGrowthAlertCard'; +import { Alert, AlertStatus } from '../types'; -/** - * The alerts below are examples of Alert implementation; the CostInsightsApi permits returning - * any implementation of the Alert type, so adopters can create their own. The CostInsightsApi - * fetches alert data from the backend, then creates Alert classes with the data. - */ +const createStatusHandler = (status?: string) => (alert: Alert) => + alert.status === status; +export const isActive = createStatusHandler(); +export const isSnoozed = createStatusHandler(AlertStatus.Snoozed); +export const isAccepted = createStatusHandler(AlertStatus.Accepted); +export const isDismissed = createStatusHandler(AlertStatus.Dismissed); -export class UnlabeledDataflowAlert implements Alert { - data: UnlabeledDataflowData; +export const sumOfAllAlerts = (sum: number, alerts: Alert[]) => + sum + alerts.length; - constructor(data: UnlabeledDataflowData) { - this.data = data; - } - - title = 'Add labels to workflows'; - subtitle = - 'Labels show in billing data, enabling cost insights for each workflow.'; - url = '/cost-insights/labeling-jobs'; - - get element() { - return ; - } -} - -export class ProjectGrowthAlert implements Alert { - data: ProjectGrowthData; - - constructor(data: ProjectGrowthData) { - this.data = data; - } - - get title() { - return `Investigate cost growth in project ${this.data.project}`; - } - - subtitle = - 'Cost growth outpacing business growth is unsustainable long-term.'; - url = '/cost-insights/investigating-growth'; - - get element() { - return ; - } +export function choose( + status: readonly [boolean, boolean, boolean], + values: [T, T, T], +): T | null { + const i = status.indexOf(true); + return i < 0 ? null : values[i]; } diff --git a/plugins/cost-insights/src/utils/duration.ts b/plugins/cost-insights/src/utils/duration.ts index 6eebead1d7..79ea150fc5 100644 --- a/plugins/cost-insights/src/utils/duration.ts +++ b/plugins/cost-insights/src/utils/duration.ts @@ -31,6 +31,7 @@ export function inclusiveStartDateOf( exclusiveEndDate: string, ): string { switch (duration) { + case Duration.P7D: case Duration.P30D: case Duration.P90D: return moment(exclusiveEndDate) @@ -53,6 +54,7 @@ export function exclusiveEndDateOf( inclusiveEndDate: string, ): string { switch (duration) { + case Duration.P7D: case Duration.P30D: case Duration.P90D: return moment(inclusiveEndDate) @@ -80,8 +82,15 @@ export function inclusiveEndDateOf( } // https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals -export function intervalsOf(duration: Duration, inclusiveEndDate: string) { - return `R2/${duration}/${exclusiveEndDateOf(duration, inclusiveEndDate)}`; +export function intervalsOf( + duration: Duration, + inclusiveEndDate: string, + repeating: number = 2, +) { + return `R${repeating}/${duration}/${exclusiveEndDateOf( + duration, + inclusiveEndDate, + )}`; } export function quarterEndDate(inclusiveEndDate: string): string { diff --git a/plugins/cost-insights/src/utils/loading.ts b/plugins/cost-insights/src/utils/loading.ts index 3328f23cfc..d15101075d 100644 --- a/plugins/cost-insights/src/utils/loading.ts +++ b/plugins/cost-insights/src/utils/loading.ts @@ -29,6 +29,7 @@ export enum DefaultLoadingAction { CostInsightsInitial = 'cost-insights-initial', CostInsightsPage = 'cost-insights-page', CostInsightsProducts = 'cost-insights-products', + CostInsightsAlerts = 'cost-insights-alerts', } export const INITIAL_LOADING_ACTIONS = [ diff --git a/plugins/cost-insights/src/utils/styles.ts b/plugins/cost-insights/src/utils/styles.ts index c04ceba8e3..acb900c490 100644 --- a/plugins/cost-insights/src/utils/styles.ts +++ b/plugins/cost-insights/src/utils/styles.ts @@ -479,8 +479,8 @@ export const useSelectStyles = makeStyles( }), ); -export const useAlertActionCardStyles = makeStyles( - (theme: BackstageTheme) => +export const useActionItemCardStyles = makeStyles( + (theme: CostInsightsTheme) => createStyles({ card: { boxShadow: 'none', @@ -489,15 +489,12 @@ export const useAlertActionCardStyles = makeStyles( backgroundColor: theme.palette.textVerySubtle, color: theme.palette.text.primary, }, - }), -); - -export const useAlertActionCardHeader = makeStyles( - (theme: CostInsightsTheme) => - createStyles({ root: { + minHeight: 80, paddingBottom: theme.spacing(2), borderRadius: theme.shape.borderRadius, + }, + activeRoot: { cursor: 'pointer', transition: theme.transitions.create('background', { duration: theme.transitions.duration.short, @@ -599,3 +596,31 @@ export const useEntityDialogStyles = makeStyles(theme => }, }), ); + +export const useAlertDialogStyles = makeStyles((theme: BackstageTheme) => + createStyles({ + content: { + padding: theme.spacing(0, 5, 2, 5), + }, + actions: { + padding: theme.spacing(2, 5), + }, + radio: { + margin: theme.spacing(-0.5, 0, -0.5, 0), + }, + icon: { + color: theme.palette.primary.dark, + margin: theme.spacing(2.5, 2.5, 0, 0), + padding: 0, + }, + }), +); + +export const useAlertStatusSummaryButtonStyles = makeStyles(() => ({ + icon: { + transform: 'transform 5s', + }, + clicked: { + transform: 'rotate(180deg)', + }, +})); diff --git a/plugins/cost-insights/src/utils/tests.tsx b/plugins/cost-insights/src/utils/tests.tsx index 852330acc3..13b38a49f2 100644 --- a/plugins/cost-insights/src/utils/tests.tsx +++ b/plugins/cost-insights/src/utils/tests.tsx @@ -22,6 +22,7 @@ import { IdentityApi, identityApiRef, } from '@backstage/core'; +import { AlertsContext, AlertsContextProps } from '../hooks/useAlerts'; import { LoadingContext, LoadingContextProps } from '../hooks/useLoading'; import { GroupsContext, GroupsContextProps } from '../hooks/useGroups'; import { FilterContext, FilterContextProps } from '../hooks/useFilters'; @@ -163,8 +164,9 @@ export type MockScrollProviderProps = PropsWithChildren<{}>; export const MockScrollProvider = ({ children }: MockScrollProviderProps) => { const defaultContext: ScrollContextProps = { - scrollTo: null, - setScrollTo: jest.fn(), + scroll: null, + setScroll: jest.fn(), + ScrollAnchor: jest.fn(() =>
), }; return ( @@ -231,3 +233,28 @@ export const MockCostInsightsApiProvider = ({ return {children}; }; + +export type MockAlertsProviderContextProps = PartialPropsWithChildren< + AlertsContextProps +>; + +export const MockAlertsProvider = ({ + children, + ...context +}: MockAlertsProviderContextProps) => { + const defaultContext: AlertsContextProps = { + alerts: { + alerts: [], + snoozed: null, + accepted: null, + dismissed: null, + }, + setAlerts: jest.fn(), + }; + + return ( + + {children} + + ); +}; From 19172f5a9b7e5b94b6c93bbe74bcc645fbdec789 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Mon, 25 Jan 2021 18:04:31 -0500 Subject: [PATCH 04/99] changeset --- .changeset/cost-insights-tricky-moles-grin.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cost-insights-tricky-moles-grin.md diff --git a/.changeset/cost-insights-tricky-moles-grin.md b/.changeset/cost-insights-tricky-moles-grin.md new file mode 100644 index 0000000000..c35769e03c --- /dev/null +++ b/.changeset/cost-insights-tricky-moles-grin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': minor +--- + +add alert hooks From c81687ade11ae5f852a6482fa99d0135bfd93b37 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Mon, 25 Jan 2021 18:56:10 -0500 Subject: [PATCH 05/99] cleanup --- .../src/alerts/KubernetesMigrationAlert.tsx | 35 ++++++------- plugins/cost-insights/src/client.ts | 11 ++-- .../components/ActionItems/ActionItems.tsx | 52 +++++++++---------- .../AlertInsights/AlertDialog.test.tsx | 20 +++---- .../src/forms/AlertDismissForm.tsx | 8 +-- 5 files changed, 59 insertions(+), 67 deletions(-) diff --git a/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx b/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx index 8940322223..d7cc5ac79d 100644 --- a/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx +++ b/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx @@ -42,18 +42,19 @@ export interface MigrationAlert extends Alert { } /** - * The alert below is an example of an Alert implementation using event hooks. + * This is an example of an Alert implementation using optional event hooks. * - * Alerts can be customized to be accepted, dismissed snoozed or any combination - * by defining a corresponding hook on the alert instance. + * Event hooks can be used to enable users to dismiss, snooze, or accept an action item + * - or any combination thereof. Defining a hook will generate default UI - button, dialog and + * form. Cost Insights does not preserve client side alert state - each hook is expected to return a new set of alerts. * - * For example, defining an onDismissed hook will render a dismiss button that, when clicked, will - * generate a dialog prompting the user to provide a reason for dismissing the alert. - * Dismiss form data will be passed to the hook, which must eventually return a new set of alerts. - * Errors thrown within hooks will generate a snackbar, which can be used to display a - * user-friendly error message. + * Snoozed, accepted, etc. alerts should define a corresponding status property. Alerts will be aggregated + * by status in a collapsed view below Alert Insights section and a badge will appear in Action Items + * showing the total alerts of that status. * - * Cost Insights provides default forms for each hook, which can be overriden by providing a custom form component. + * Default forms can be overriden by providing a valid React form component. Form components + * must return valid form elements, and accept a ref and onSubmit event handler. See /forms + * for example implementations. Custom forms must implement a corresponding event hook. */ export class KubernetesMigrationAlert implements MigrationAlert { @@ -63,9 +64,7 @@ export class KubernetesMigrationAlert implements MigrationAlert { subtitle = 'Services running on Kubernetes are estimated to save 50% or more compared to Compute Engine.'; - // Override default dismiss form with custom form component. - // SnoozeForm: AlertForm = MigrationSnoozeForm; - // AcceptForm: AlertForm = MigrationAcceptForm; + // Override default dismiss form with a custom form component. DismissForm: AlertForm< MigrationAlert, MigrationDismissFormData @@ -101,7 +100,7 @@ export class KubernetesMigrationAlert implements MigrationAlert { ); } - /* Displays a custom dismiss form. */ + /* Fires when the onSubmit event is raised on a DismissAlert form. Displays a custom dismiss form. */ async onDismissed( options: AlertOptions, ): Promise { @@ -112,17 +111,13 @@ export class KubernetesMigrationAlert implements MigrationAlert { { title: this.title, subtitle: this.subtitle, - /** - * If a status property is defined, the alert will be filtered from the action items list - * but still appear grouped with other action items of the same status in the Hidden Action Items section. - */ status: AlertStatus.Dismissed, }, ]), ); } - /* Displays default accept form. */ + /* Fires when the onSubmit event is raised on an SnoozeAlert form. Displays default snooze form. */ async onSnoozed( options: AlertOptions, ): Promise { @@ -139,8 +134,8 @@ export class KubernetesMigrationAlert implements MigrationAlert { ); } - /* Displays default accept form. */ - async onAccepted(options: AlertOptions): Promise { + /* Fires when the onSubmit event is raised on an AcceptAlert form. Displays default accept form. */ + async onAccepted(options: AlertOptions): Promise { const alerts = await this.api.getAlerts(options.group); return new Promise(resolve => setTimeout(resolve, 750, [ diff --git a/plugins/cost-insights/src/client.ts b/plugins/cost-insights/src/client.ts index 837cd7a560..dd53d1f942 100644 --- a/plugins/cost-insights/src/client.ts +++ b/plugins/cost-insights/src/client.ts @@ -172,12 +172,13 @@ export class ExampleCostInsightsClient implements CostInsightsApi { ], }; + const today = dayjs(); const alerts: Alert[] = await this.request({ group }, [ new ProjectGrowthAlert(projectGrowthData), new UnlabeledDataflowAlert(unlabeledDataflowData), new KubernetesMigrationAlert(this, { - startDate: '2021-01-24', - endDate: '2020-02-24', + startDate: today.format(DEFAULT_DATE_FORMAT), + endDate: today.add(30, 'day').format(DEFAULT_DATE_FORMAT), change: { ratio: 0, amount: 0, @@ -187,7 +188,7 @@ export class ExampleCostInsightsClient implements CostInsightsApi { id: 'service-a', aggregation: [20_000, 10_000], change: { - ratio: -1, + ratio: -0.5, amount: -10_000, }, entities: {}, @@ -196,8 +197,8 @@ export class ExampleCostInsightsClient implements CostInsightsApi { id: 'service-b', aggregation: [30_000, 15_000], change: { - ratio: -1, - amount: 15_000, + ratio: -0.5, + amount: -15_000, }, entities: {}, }, diff --git a/plugins/cost-insights/src/components/ActionItems/ActionItems.tsx b/plugins/cost-insights/src/components/ActionItems/ActionItems.tsx index 320ebb38aa..4fbe3470b1 100644 --- a/plugins/cost-insights/src/components/ActionItems/ActionItems.tsx +++ b/plugins/cost-insights/src/components/ActionItems/ActionItems.tsx @@ -32,6 +32,32 @@ import { Alert, AlertStatus } from '../../types'; import { useScroll, ScrollType } from '../../hooks'; import { useActionItemCardStyles as useStyles } from '../../utils/styles'; +type AlertStatusButtonProps = { + title: string; + amount: number; + icon: JSX.Element; + onClick: MouseEventHandler; +} & IconButtonProps; + +const AlertStatusButton = ({ + title, + amount, + icon, + onClick, + ...buttonProps +}: AlertStatusButtonProps) => ( + + + {icon} + + +); + type ActionItemsProps = { active: Alert[]; snoozed: Alert[]; @@ -104,29 +130,3 @@ export const ActionItems = ({ ); }; - -type AlertStatusButtonProps = { - title: string; - amount: number; - icon: JSX.Element; - onClick: MouseEventHandler; -} & IconButtonProps; - -const AlertStatusButton = ({ - title, - amount, - icon, - onClick, - ...buttonProps -}: AlertStatusButtonProps) => ( - - - {icon} - - -); diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertDialog.test.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertDialog.test.tsx index 5785592aeb..2e4cb53681 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertDialog.test.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertDialog.test.tsx @@ -75,8 +75,8 @@ describe('', () => { snoozed={snoozableAlert} accepted={null} dismissed={null} - onClose={jest.fn} - onSubmit={jest.fn} + onClose={jest.fn()} + onSubmit={jest.fn()} />, ); expect(getByText('For how long?')).toBeInTheDocument(); @@ -97,8 +97,8 @@ describe('', () => { snoozed={customSnoozeAlert} accepted={null} dismissed={null} - onClose={jest.fn} - onSubmit={jest.fn} + onClose={jest.fn()} + onSubmit={jest.fn()} />, ); expect(getByText('You. Complete. Me.')).toBeInTheDocument(); @@ -116,8 +116,8 @@ describe('', () => { snoozed={null} accepted={null} dismissed={dimissableAlert} - onClose={jest.fn} - onSubmit={jest.fn} + onClose={jest.fn()} + onSubmit={jest.fn()} />, ); expect(getByText('Dismiss this action item?')).toBeInTheDocument(); @@ -137,8 +137,8 @@ describe('', () => { snoozed={null} accepted={null} dismissed={customDismissAlert} - onClose={jest.fn} - onSubmit={jest.fn} + onClose={jest.fn()} + onSubmit={jest.fn()} />, ); expect(getByText('Dismiss this action item?')).toBeInTheDocument(); @@ -156,8 +156,8 @@ describe('', () => { snoozed={null} accepted={customAcceptAlert} dismissed={null} - onClose={jest.fn} - onSubmit={jest.fn} + onClose={jest.fn()} + onSubmit={jest.fn()} />, ); expect(getByText('Accept this action item?')).toBeInTheDocument(); diff --git a/plugins/cost-insights/src/forms/AlertDismissForm.tsx b/plugins/cost-insights/src/forms/AlertDismissForm.tsx index 1b3cbdb363..4271785958 100644 --- a/plugins/cost-insights/src/forms/AlertDismissForm.tsx +++ b/plugins/cost-insights/src/forms/AlertDismissForm.tsx @@ -55,18 +55,14 @@ export const AlertDismissForm = forwardRef< ); const onFormSubmit: FormEventHandler = e => { - function submit() { + e.preventDefault(); + if (reason) { onSubmit({ other: other, reason: reason, feedback: feedback, }); } - - e.preventDefault(); - if (reason) { - submit(); - } }; const onReasonChange = (_: ChangeEvent, value: string) => { From 09771bdf4633b2226dfc0bdd0c32f52223af7d13 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Mon, 25 Jan 2021 20:44:41 -0500 Subject: [PATCH 06/99] fix import; don't make type public --- plugins/cost-insights/src/alerts/index.ts | 1 + .../src/components/MigrationAlertCard/MigrationAlertCard.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/cost-insights/src/alerts/index.ts b/plugins/cost-insights/src/alerts/index.ts index eb4a079e90..b55c012a60 100644 --- a/plugins/cost-insights/src/alerts/index.ts +++ b/plugins/cost-insights/src/alerts/index.ts @@ -18,3 +18,4 @@ export { ProjectGrowthAlert } from './ProjectGrowthAlert'; export { UnlabeledDataflowAlert } from './UnlabeledDataflowAlert'; export { KubernetesMigrationAlert } from './KubernetesMigrationAlert'; export type { MigrationAlert } from './KubernetesMigrationAlert'; +export type { MigrationData } from './KubernetesMigrationAlert'; diff --git a/plugins/cost-insights/src/components/MigrationAlertCard/MigrationAlertCard.tsx b/plugins/cost-insights/src/components/MigrationAlertCard/MigrationAlertCard.tsx index f4d174d521..56d636d8aa 100644 --- a/plugins/cost-insights/src/components/MigrationAlertCard/MigrationAlertCard.tsx +++ b/plugins/cost-insights/src/components/MigrationAlertCard/MigrationAlertCard.tsx @@ -19,7 +19,7 @@ import { Box } from '@material-ui/core'; import { InfoCard } from '@backstage/core'; import { MigrationBarChartLegend } from './MigrationBarChartLegend'; import { MigrationBarChart } from './MigrationBarChart'; -import { MigrationData } from '../../types'; +import { MigrationData } from '../../alerts'; type MigrationAlertProps = { data: MigrationData; From a79134c7f1f30b47e4ec8c2c082a607db7a82278 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Mon, 25 Jan 2021 20:45:01 -0500 Subject: [PATCH 07/99] add duration --- plugins/cost-insights/src/utils/currency.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/cost-insights/src/utils/currency.ts b/plugins/cost-insights/src/utils/currency.ts index f1d67a14e4..8adbc4dcaa 100644 --- a/plugins/cost-insights/src/utils/currency.ts +++ b/plugins/cost-insights/src/utils/currency.ts @@ -20,6 +20,7 @@ export const rateOf = (cost: number, duration: Duration) => { switch (duration) { case Duration.P30D: return cost / 12; + case Duration.P7D: case Duration.P90D: case Duration.P3M: return cost / 4; From f19e4a5837fd99c0e832aff8fe5bbacd9fded7a0 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Mon, 25 Jan 2021 20:59:48 -0500 Subject: [PATCH 08/99] Update plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx Co-authored-by: Adam Harvey <33203301+adamdmharvey@users.noreply.github.com> --- plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx b/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx index d7cc5ac79d..7b775a4a96 100644 --- a/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx +++ b/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx @@ -52,7 +52,7 @@ export interface MigrationAlert extends Alert { * by status in a collapsed view below Alert Insights section and a badge will appear in Action Items * showing the total alerts of that status. * - * Default forms can be overriden by providing a valid React form component. Form components + * Default forms can be overridden by providing a valid React form component. Form components * must return valid form elements, and accept a ref and onSubmit event handler. See /forms * for example implementations. Custom forms must implement a corresponding event hook. */ From 027ee1a7cfa9a6bf7b168a9b793eefe0dc240d27 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Tue, 26 Jan 2021 17:03:14 -0500 Subject: [PATCH 09/99] add support for disabling alert dialog forms --- .../src/alerts/KubernetesMigrationAlert.tsx | 23 +- .../AlertInsights/AlertDialog.test.tsx | 297 +++++++++--------- .../components/AlertInsights/AlertDialog.tsx | 106 ++++--- plugins/cost-insights/src/types/Alert.ts | 6 +- plugins/cost-insights/src/utils/alerts.tsx | 4 +- 5 files changed, 239 insertions(+), 197 deletions(-) diff --git a/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx b/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx index 7b775a4a96..8c59d0c1a5 100644 --- a/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx +++ b/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx @@ -52,9 +52,15 @@ export interface MigrationAlert extends Alert { * by status in a collapsed view below Alert Insights section and a badge will appear in Action Items * showing the total alerts of that status. * - * Default forms can be overridden by providing a valid React form component. Form components - * must return valid form elements, and accept a ref and onSubmit event handler. See /forms - * for example implementations. Custom forms must implement a corresponding event hook. + * Customizing Alerts + * Default forms can be overridden in two ways - by setting a form property to null or defining a custom component. + * + * If a form property is set to null, the Dialog will not render a form. This can be useful in scenarios + * where data isn't needed from the user such as when a user accepts an action item's recommendation. + * + * If a form property is set to a React component, the Dialog will render the form component in place of the default form. + * Form components must return valid form elements, and accept a ref and onSubmit event handler. + * Custom forms must implement the corresponding event hook. See /forms for example implementations. */ export class KubernetesMigrationAlert implements MigrationAlert { @@ -64,7 +70,10 @@ export class KubernetesMigrationAlert implements MigrationAlert { subtitle = 'Services running on Kubernetes are estimated to save 50% or more compared to Compute Engine.'; - // Override default dismiss form with a custom form component. + // Dialog will not render a form if form property set to null. + AcceptForm = null; + + // Overrides default Dismiss form with a custom form component. DismissForm: AlertForm< MigrationAlert, MigrationDismissFormData @@ -100,7 +109,7 @@ export class KubernetesMigrationAlert implements MigrationAlert { ); } - /* Fires when the onSubmit event is raised on a DismissAlert form. Displays a custom dismiss form. */ + /* Fires when the onSubmit event is raised on a Dismiss form. Displays custom dismiss form. */ async onDismissed( options: AlertOptions, ): Promise { @@ -117,7 +126,7 @@ export class KubernetesMigrationAlert implements MigrationAlert { ); } - /* Fires when the onSubmit event is raised on an SnoozeAlert form. Displays default snooze form. */ + /* Fires when the onSubmit event is raised on a Snooze form. Displays default snooze form. */ async onSnoozed( options: AlertOptions, ): Promise { @@ -134,7 +143,7 @@ export class KubernetesMigrationAlert implements MigrationAlert { ); } - /* Fires when the onSubmit event is raised on an AcceptAlert form. Displays default accept form. */ + /* Fires when the Accept button is clicked. Dialog does not render a form. See KubernetesMigrationAlert.AcceptForm */ async onAccepted(options: AlertOptions): Promise { const alerts = await this.api.getAlerts(options.group); return new Promise(resolve => diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertDialog.test.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertDialog.test.tsx index 2e4cb53681..72ff757cee 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertDialog.test.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertDialog.test.tsx @@ -16,154 +16,169 @@ import React from 'react'; import { AlertDialog } from './AlertDialog'; import { render } from '@testing-library/react'; -import { - Alert, - AlertFormProps, - AlertSnoozeOptions, - AlertDismissOptions, -} from '../../types'; +import { Alert, AlertFormProps } from '../../types'; type MockFormDataProps = AlertFormProps; -const MockForm = React.forwardRef( - (props, ref) => ( +function createForm(title: string) { + return React.forwardRef((props, ref) => (
- You. Complete. Me. + You. {title}. Me.
- ), -); + )); +} +const snoozableAlert: Alert = { + title: 'title', + subtitle: 'test-subtitle', + onSnoozed: jest.fn(), +}; + +const dimissableAlert: Alert = { + title: 'title', + subtitle: 'subtitle', + onDismissed: jest.fn(), +}; + +const acceptAlert: Alert = { + title: 'title', + subtitle: 'subtitle', + onAccepted: jest.fn(), +}; + +const customSnoozeAlert: Alert = { + title: 'title', + subtitle: 'subtitle', + onSnoozed: jest.fn(), + SnoozeForm: createForm('Snooze'), +}; + +const customDismissAlert: Alert = { + title: 'title', + subtitle: 'subtitle', + onDismissed: jest.fn(), + DismissForm: createForm('Dismiss'), +}; + +const customAcceptAlert: Alert = { + title: 'title', + subtitle: 'test-subtitle', + onAccepted: jest.fn(), + AcceptForm: createForm('Accept'), +}; + +const nullAcceptAlert: Alert = { + title: 'title', + subtitle: 'test-subtitle', + onAccepted: jest.fn(), + AcceptForm: null, +}; + +const nullDismissAlert: Alert = { + title: 'title', + subtitle: 'test-subtitle', + onDismissed: jest.fn(), + DismissForm: null, +}; + +const nullSnoozeAlert: Alert = { + title: 'title', + subtitle: 'test-subtitle', + onSnoozed: jest.fn(), + SnoozeForm: null, +}; describe('', () => { - const snoozableAlert: Alert = { - title: 'title', - subtitle: 'test-subtitle', - onSnoozed: jest.fn(), - }; + describe.each` + accepted | dismissed | snoozed | action | text + ${acceptAlert} | ${null} | ${null} | ${['Accept', 'accepted']} | ${'My team can commit to making this change soon, or has already.'} + ${null} | ${dimissableAlert} | ${null} | ${['Dismiss', 'dismissed']} | ${'Reason for dismissing?'} + ${null} | ${null} | ${snoozableAlert} | ${['Snooze', 'snoozed']} | ${'For how long?'} + `( + 'Default forms', + ({ accepted, dismissed, snoozed, action: [action, actioned], text }) => { + it(`Displays a default ${action} form`, () => { + const { getByText } = render( + , + ); + expect(getByText(text)).toBeInTheDocument(); + expect(getByText(`${action} this action item?`)).toBeInTheDocument(); + expect( + getByText(`This action item will be ${actioned} for all of Ramones.`), + ).toBeInTheDocument(); + }); + }, + ); - const dimissableAlert: Alert = { - title: 'title', - subtitle: 'subtitle', - onDismissed: jest.fn(), - }; + describe.each` + accepted | dismissed | snoozed | action + ${customAcceptAlert} | ${null} | ${null} | ${['Accept', 'accepted']} + ${null} | ${customDismissAlert} | ${null} | ${['Dismiss', 'dismissed']} + ${null} | ${null} | ${customSnoozeAlert} | ${['Snooze', 'snoozed']} + `( + 'Custom forms', + ({ accepted, dismissed, snoozed, action: [Action, actioned] }) => { + it(`Displays a custom ${Action} form`, () => { + const { getByText } = render( + , + ); + expect(getByText(`You. ${Action}. Me.`)).toBeInTheDocument(); + expect(getByText(`${Action} this action item?`)).toBeInTheDocument(); + expect( + getByText(`This action item will be ${actioned} for all of Ramones.`), + ).toBeInTheDocument(); + }); + }, + ); - const customSnoozeAlert: Alert = { - title: 'title', - subtitle: 'subtitle', - onSnoozed: jest.fn(), - SnoozeForm: MockForm, - }; - - const customDismissAlert: Alert = { - title: 'title', - subtitle: 'subtitle', - onDismissed: jest.fn(), - DismissForm: MockForm, - }; - - const customAcceptAlert: Alert = { - title: 'title', - subtitle: 'test-subtitle', - onAccepted: jest.fn(), - AcceptForm: MockForm, - }; - - it('Displays a default snooze form', () => { - const { getByText } = render( - , - ); - expect(getByText('For how long?')).toBeInTheDocument(); - expect(getByText('Snooze this action item?')).toBeInTheDocument(); - expect( - getByText('This action item will be snoozed for all of Ramones.'), - ).toBeInTheDocument(); - AlertSnoozeOptions.forEach(a => - expect(getByText(a.label)).toBeInTheDocument(), - ); - }); - - it('Displays a custom snooze form', () => { - const { getByText } = render( - , - ); - expect(getByText('You. Complete. Me.')).toBeInTheDocument(); - expect(getByText('Snooze this action item?')).toBeInTheDocument(); - expect( - getByText('This action item will be snoozed for all of Ramones.'), - ).toBeInTheDocument(); - }); - - it('Displays a default dismiss form', () => { - const { getByText } = render( - , - ); - expect(getByText('Dismiss this action item?')).toBeInTheDocument(); - expect( - getByText('This action item will be dismissed for all of Ramones.'), - ).toBeInTheDocument(); - AlertDismissOptions.forEach(a => - expect(getByText(a.label)).toBeInTheDocument(), - ); - }); - - it('Displays a custom dismiss form', () => { - const { getByText } = render( - , - ); - expect(getByText('Dismiss this action item?')).toBeInTheDocument(); - expect(getByText('You. Complete. Me.')).toBeInTheDocument(); - expect( - getByText('This action item will be dismissed for all of Ramones.'), - ).toBeInTheDocument(); - }); - - it('Displays a custom accept form', () => { - const { getByText } = render( - , - ); - expect(getByText('Accept this action item?')).toBeInTheDocument(); - expect(getByText('You. Complete. Me.')).toBeInTheDocument(); - expect( - getByText('This action item will be accepted for all of Ramones.'), - ).toBeInTheDocument(); - }); + describe.each` + accepted | dismissed | snoozed | action | text + ${nullAcceptAlert} | ${null} | ${null} | ${['Accept', 'accept', 'accepted']} | ${'My team can commit to making this change soon, or has already.'} + ${null} | ${nullDismissAlert} | ${null} | ${['Dismiss', 'dismiss', 'dismissed']} | ${'Reason for dismissing?'} + ${null} | ${null} | ${nullSnoozeAlert} | ${['Snooze', 'snooze', 'snoozed']} | ${'For how long?'} + `( + 'Null forms', + ({ + accepted, + dismissed, + snoozed, + action: [Action, action, actioned], + text, + }) => { + it(`Does NOT display a ${Action} form`, () => { + const { getByText, getByRole, queryByText } = render( + , + ); + expect(queryByText(text)).not.toBeInTheDocument(); + expect(getByRole('button', { name: action })).toBeInTheDocument(); + expect(getByText(`${Action} this action item?`)).toBeInTheDocument(); + expect( + getByText(`This action item will be ${actioned} for all of Ramones.`), + ).toBeInTheDocument(); + }); + }, + ); }); diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertDialog.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertDialog.tsx index aa5b8c59fb..f7154602b9 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertDialog.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertDialog.tsx @@ -33,7 +33,7 @@ import { } from '../../forms'; import { useAlertDialogStyles as useStyles } from '../../utils/styles'; import { choose } from '../../utils/alerts'; -import { Alert, Maybe } from '../../types'; +import { Alert, AlertForm, Maybe } from '../../types'; const DEFAULT_FORM_ID = 'alert-form'; @@ -79,20 +79,41 @@ export const AlertDialog = ({ setDisabled(true); } - const SnoozeForm = snoozed?.SnoozeForm ?? AlertSnoozeForm; - const AcceptForm = accepted?.AcceptForm ?? AlertAcceptForm; - const DismissForm = dismissed?.DismissForm ?? AlertDismissForm; + const SnoozeForm: Maybe = snoozed?.SnoozeForm ?? AlertSnoozeForm; + const AcceptForm: Maybe = accepted?.AcceptForm ?? AlertAcceptForm; + const DismissForm: Maybe = + dismissed?.DismissForm ?? AlertDismissForm; - const isSnoozeFormDisplayed = !!snoozed?.onSnoozed; - const isAcceptFormDisplayed = !!accepted?.onAccepted; - const isDismissFormDisplayed = !!dismissed?.onDismissed; + const isSnoozingEnabled = !!snoozed?.onSnoozed; + const isAcceptingEnabled = !!accepted?.onAccepted; + const isDismissingEnabled = !!dismissed?.onDismissed; + + const isSnoozeFormDisabled = snoozed?.SnoozeForm === null; + const isAcceptFormDisabled = accepted?.AcceptForm === null; + const isDismissFormDisabled = dismissed?.DismissForm === null; + const isFormDisabled = + isSnoozeFormDisabled || isAcceptFormDisabled || isDismissFormDisabled; const status = [ - isAcceptFormDisplayed, - isSnoozeFormDisplayed, - isDismissFormDisplayed, + isSnoozingEnabled, + isAcceptingEnabled, + isDismissingEnabled, ] as const; + const [Action, action, actioned] = + choose(status, [ + ['Snooze', 'snooze', 'snoozed'], + ['Accept', 'accept', 'accepted'], + ['Dismiss', 'dismiss', 'dismissed'], + ]) ?? []; + + const [title, subtitle] = + choose(status, [ + [snoozed?.title, snoozed?.subtitle], + [accepted?.title, accepted?.subtitle], + [dismissed?.title, dismissed?.subtitle], + ]) ?? []; + const TransitionProps = { mountOnEnter: true, unmountOnExit: true, @@ -122,24 +143,20 @@ export const AlertDialog = ({ - + - - {choose(status, ['Accept', 'Snooze', 'Dismiss'])} this action - item? - + {Action} this action item? - This action item will be{' '} - {choose(status, ['accepted', 'snoozed', 'dismissed'])} for all of{' '} - {group}. + This action item will be {actioned} for all of {group}. @@ -152,23 +169,11 @@ export const AlertDialog = ({ borderRadius={4} > - - {choose(status, [ - accepted?.title, - snoozed?.title, - dismissed?.title, - ])} - - - - {choose(status, [ - accepted?.subtitle, - snoozed?.subtitle, - dismissed?.subtitle, - ])} + {title} + {subtitle} - {isSnoozeFormDisplayed && ( + {isSnoozingEnabled && !isSnoozeFormDisabled && ( )} - {isDismissFormDisplayed && ( + {isDismissingEnabled && !isDismissFormDisabled && ( )} - {isAcceptFormDisplayed && ( + {isAcceptingEnabled && !isAcceptFormDisabled && ( - + {isFormDisabled ? ( + + ) : ( + + )} ); diff --git a/plugins/cost-insights/src/types/Alert.ts b/plugins/cost-insights/src/types/Alert.ts index 21e1a779a8..eb8919b71a 100644 --- a/plugins/cost-insights/src/types/Alert.ts +++ b/plugins/cost-insights/src/types/Alert.ts @@ -40,9 +40,9 @@ export type Alert = { status?: AlertStatus; url?: string; buttonText?: string; // Default: View Instructions - SnoozeForm?: AlertForm; - AcceptForm?: AlertForm; - DismissForm?: AlertForm; + SnoozeForm?: Maybe; + AcceptForm?: Maybe; + DismissForm?: Maybe; onSnoozed?(options: AlertOptions): Promise; onAccepted?(options: AlertOptions): Promise; onDismissed?(options: AlertOptions): Promise; diff --git a/plugins/cost-insights/src/utils/alerts.tsx b/plugins/cost-insights/src/utils/alerts.tsx index b8dfddf79b..e10740fbfe 100644 --- a/plugins/cost-insights/src/utils/alerts.tsx +++ b/plugins/cost-insights/src/utils/alerts.tsx @@ -29,7 +29,7 @@ export const sumOfAllAlerts = (sum: number, alerts: Alert[]) => export function choose( status: readonly [boolean, boolean, boolean], values: [T, T, T], -): T | null { +): T | undefined { const i = status.indexOf(true); - return i < 0 ? null : values[i]; + return values[i]; } From d275c3eb6e9ce671cb49ec8da7c97d6b0a1b9c9d Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Tue, 26 Jan 2021 19:05:35 -0500 Subject: [PATCH 10/99] Update ActionItems.test.tsx --- .../src/components/ActionItems/ActionItems.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cost-insights/src/components/ActionItems/ActionItems.test.tsx b/plugins/cost-insights/src/components/ActionItems/ActionItems.test.tsx index 8c96e47f1a..677ba99c6a 100644 --- a/plugins/cost-insights/src/components/ActionItems/ActionItems.test.tsx +++ b/plugins/cost-insights/src/components/ActionItems/ActionItems.test.tsx @@ -24,7 +24,7 @@ function renderInContext(children: JSX.Element) { } describe('', () => { - it('should not display status buttons if there no active alerts', () => { + it('should not display status buttons if there are no active alerts', () => { const { queryByRole } = renderInContext( , ); From 8180fb76d8ea9b294bdfe595d45a4bedc6a23397 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Wed, 27 Jan 2021 15:32:29 -0500 Subject: [PATCH 11/99] memoize alerts, not context --- .../CostInsightsPage/CostInsightsPage.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx index e7377eca10..f974fa0ddd 100644 --- a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx +++ b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx @@ -78,10 +78,18 @@ export const CostInsightsPage = () => { const { pageFilters, setPageFilters } = useFilters(p => p); - const snoozed = useMemo(() => alerts.alerts.filter(isSnoozed), [alerts]); - const accepted = useMemo(() => alerts.alerts.filter(isAccepted), [alerts]); - const dismissed = useMemo(() => alerts.alerts.filter(isDismissed), [alerts]); - const activeAlerts = useMemo(() => alerts.alerts.filter(isActive), [alerts]); + const snoozed = useMemo(() => alerts.alerts.filter(isSnoozed), [ + alerts.alerts, + ]); + const accepted = useMemo(() => alerts.alerts.filter(isAccepted), [ + alerts.alerts, + ]); + const dismissed = useMemo(() => alerts.alerts.filter(isDismissed), [ + alerts.alerts, + ]); + const activeAlerts = useMemo(() => alerts.alerts.filter(isActive), [ + alerts.alerts, + ]); const isActionItemsDisplayed = !!activeAlerts.length; const isAlertInsightsDisplayed = !!alerts.alerts.length; From 60ace107797e949afdb3fb320b49c6e2d2edc4b8 Mon Sep 17 00:00:00 2001 From: Nigel Wright Date: Thu, 28 Jan 2021 20:19:30 +1300 Subject: [PATCH 12/99] Fix: added a check for invalid timezone --- .../src/layout/HomepageTimer/HomepageTimer.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/core/src/layout/HomepageTimer/HomepageTimer.tsx b/packages/core/src/layout/HomepageTimer/HomepageTimer.tsx index 59b7f3fb3d..219665599c 100644 --- a/packages/core/src/layout/HomepageTimer/HomepageTimer.tsx +++ b/packages/core/src/layout/HomepageTimer/HomepageTimer.tsx @@ -39,18 +39,28 @@ function getTimes(configApi: ConfigApi) { for (const clock of clockConfigs) { if (clock.has('label') && clock.has('timezone')) { + let label = clock.getString('label'); + const options = { timeZone: clock.getString('timezone'), ...timeFormat, }; - const time = d.toLocaleTimeString(lang, options); - const label = clock.getString('label'); + try { + new Date().toLocaleString(lang, options); + } catch (e) { + // eslint-disable-next-line no-console + console.warn( + `The timezone ${options.timeZone} is invalid. Defaulting to America/Los Angeles`, + ); + options.timeZone = 'America/Los_Angeles'; + label = 'Los Angeles'; + } + const time = d.toLocaleTimeString(lang, options); clocks.push({ time, label }); } } - return clocks; } From 90c8f20b9ff0caaef0bcb3c9591a6b1fa137972c Mon Sep 17 00:00:00 2001 From: Niall McCullagh Date: Thu, 28 Jan 2021 14:19:24 +0000 Subject: [PATCH 13/99] fix(tech-radar): fix mapping of moved and url atrributes to fix display As part of the refactor to include entry history the moved and url attributes were no longer being mapped. This resulted in the radar not indicating if an entry had moved, and the bubble not being clickable. --- .changeset/shaggy-dingos-suffer.md | 5 +++++ plugins/tech-radar/src/components/RadarComponent.tsx | 4 +++- plugins/tech-radar/src/sampleData.ts | 6 +++--- 3 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 .changeset/shaggy-dingos-suffer.md diff --git a/.changeset/shaggy-dingos-suffer.md b/.changeset/shaggy-dingos-suffer.md new file mode 100644 index 0000000000..66c520bb3e --- /dev/null +++ b/.changeset/shaggy-dingos-suffer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-radar': patch +--- + +Fix mapping RadarEntry and Entry for moved and url attributes diff --git a/plugins/tech-radar/src/components/RadarComponent.tsx b/plugins/tech-radar/src/components/RadarComponent.tsx index 583a7c91e0..759fec62c3 100644 --- a/plugins/tech-radar/src/components/RadarComponent.tsx +++ b/plugins/tech-radar/src/components/RadarComponent.tsx @@ -59,7 +59,7 @@ const RadarComponent = (props: TechRadarComponentProps): JSX.Element => { ring: loaderResponse!.rings.find( r => r.id === entry.timeline[0].ringId, )!, - history: entry.timeline.map(e => { + timeline: entry.timeline.map(e => { return { date: e.date, ring: loaderResponse!.rings.find(a => a.id === e.ringId)!, @@ -67,6 +67,8 @@ const RadarComponent = (props: TechRadarComponentProps): JSX.Element => { moved: e.moved, }; }), + moved: entry.timeline[0].moved, + url: entry.url, }; }); }; diff --git a/plugins/tech-radar/src/sampleData.ts b/plugins/tech-radar/src/sampleData.ts index 914bc30931..613e7b3cdc 100644 --- a/plugins/tech-radar/src/sampleData.ts +++ b/plugins/tech-radar/src/sampleData.ts @@ -53,7 +53,7 @@ entries.push({ entries.push({ timeline: [ { - moved: 0, + moved: -1, ringId: 'use', date: new Date('2020-08-06'), description: @@ -69,7 +69,7 @@ entries.push({ entries.push({ timeline: [ { - moved: 0, + moved: 1, ringId: 'use', date: new Date('2020-08-06'), }, @@ -88,7 +88,7 @@ entries.push({ date: new Date('2020-08-06'), }, ], - url: '#', + url: 'https://reactjs.org/', key: 'react', id: 'react', title: 'React', From 9c7efe153567c0545190128e64d92428c9c21115 Mon Sep 17 00:00:00 2001 From: Niall McCullagh Date: Thu, 28 Jan 2021 16:59:05 +0000 Subject: [PATCH 14/99] fix(tech-radar): allow entries on radar legend to be clickable --- .changeset/shaggy-dingos-suffer.md | 1 + plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.changeset/shaggy-dingos-suffer.md b/.changeset/shaggy-dingos-suffer.md index 66c520bb3e..c1e200b701 100644 --- a/.changeset/shaggy-dingos-suffer.md +++ b/.changeset/shaggy-dingos-suffer.md @@ -3,3 +3,4 @@ --- Fix mapping RadarEntry and Entry for moved and url attributes +Fix clicking of links in the radar legend diff --git a/plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx b/plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx index e319846d78..e10993b010 100644 --- a/plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx +++ b/plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx @@ -71,12 +71,12 @@ const useStyles = makeStyles(theme => ({ 'font-feature-settings': 'pnum', }, entry: { - pointerEvents: 'none', + pointerEvents: 'visiblePainted', userSelect: 'none', fontSize: '11px', }, entryLink: { - pointerEvents: 'none', + pointerEvents: 'visiblePainted', }, })); From e1e650e12a04345ff18d3e1072a3290595e3c98d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 Jan 2021 19:02:32 +0100 Subject: [PATCH 15/99] core-api: if loginPopup is rejected, throw PopupRejectedError --- packages/core-api/src/lib/loginPopup.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core-api/src/lib/loginPopup.ts b/packages/core-api/src/lib/loginPopup.ts index 2e14447882..425998f4bc 100644 --- a/packages/core-api/src/lib/loginPopup.ts +++ b/packages/core-api/src/lib/loginPopup.ts @@ -82,7 +82,9 @@ export function showLoginPopup(options: LoginPopupOptions): Promise { let targetOrigin = ''; if (!popup || typeof popup.closed === 'undefined' || popup.closed) { - reject(new Error('Failed to open auth popup.')); + const error = new Error('Failed to open auth popup.'); + error.name = 'PopupRejectedError'; + reject(error); return; } From a68827420942f5a13f478fc54c7401bcbf2dbfa0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 Jan 2021 19:03:26 +0100 Subject: [PATCH 16/99] core-api: fix check to determine whether popup was closed or the messaging was misconfigured --- packages/core-api/src/lib/loginPopup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-api/src/lib/loginPopup.ts b/packages/core-api/src/lib/loginPopup.ts index 425998f4bc..716c4f9651 100644 --- a/packages/core-api/src/lib/loginPopup.ts +++ b/packages/core-api/src/lib/loginPopup.ts @@ -122,7 +122,7 @@ export function showLoginPopup(options: LoginPopupOptions): Promise { const intervalId = setInterval(() => { if (popup.closed) { const errMessage = `Login failed, ${ - targetOrigin !== window.location.origin + targetOrigin && targetOrigin !== window.location.origin ? `Incorrect app origin, expected ${targetOrigin}` : 'popup was closed' }`; From 32c95605fbc3170a241582797a559797005e774b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 Jan 2021 19:06:05 +0100 Subject: [PATCH 17/99] add popup fix changeset --- .changeset/shiny-rabbits-unite.md | 5 +++++ .github/styles/vocab.txt | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/shiny-rabbits-unite.md diff --git a/.changeset/shiny-rabbits-unite.md b/.changeset/shiny-rabbits-unite.md new file mode 100644 index 0000000000..dca9ee0dd9 --- /dev/null +++ b/.changeset/shiny-rabbits-unite.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Fix check that determines whether popup was closed or the messaging was misconfigured. diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 425e6eea6f..d545b380f5 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -130,6 +130,7 @@ middleware minikube Minikube misconfiguration +misconfigured misgendering mkdocs Mkdocs From 01058d486c074c3874243562786370010b85a605 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 Jan 2021 19:13:01 +0100 Subject: [PATCH 18/99] core: add separate prop union for SignInPage for using a single provider --- .../core/src/layout/SignInPage/SignInPage.tsx | 105 ++++++++++++++++-- 1 file changed, 98 insertions(+), 7 deletions(-) diff --git a/packages/core/src/layout/SignInPage/SignInPage.tsx b/packages/core/src/layout/SignInPage/SignInPage.tsx index 83ef8fe8a8..c3e5b75bb7 100644 --- a/packages/core/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core/src/layout/SignInPage/SignInPage.tsx @@ -14,30 +14,38 @@ * limitations under the License. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Page } from '../Page'; import { Header } from '../Header'; import { Content } from '../Content/Content'; import { ContentHeader } from '../ContentHeader/ContentHeader'; -import { Grid } from '@material-ui/core'; +import { Grid, Button, Typography } from '@material-ui/core'; import { SignInPageProps, useApi, configApiRef } from '@backstage/core-api'; import { useSignInProviders, getSignInProviders } from './providers'; -import { IdentityProviders } from './types'; +import { IdentityProviders, SignInConfig } from './types'; import { Progress } from '../../components/Progress'; -import { useStyles } from './styles'; +import { GridItem, useStyles } from './styles'; +import { InfoCard } from '../InfoCard'; -export type Props = SignInPageProps & { +type MultiSignInPageProps = SignInPageProps & { providers: IdentityProviders; title?: string; align?: 'center' | 'left'; }; -export const SignInPage = ({ +type SingleSignInPageProps = SignInPageProps & { + provider: SignInConfig; + auto?: boolean; +}; + +export type Props = MultiSignInPageProps | SingleSignInPageProps; + +export const MultiSignInPage = ({ onResult, providers = [], title, align = 'left', -}: Props) => { +}: MultiSignInPageProps) => { const configApi = useApi(configApiRef); const classes = useStyles(); @@ -69,3 +77,86 @@ export const SignInPage = ({ ); }; + +export const SingleSignInPage = ({ + onResult, + provider, + auto, +}: SingleSignInPageProps) => { + const classes = useStyles(); + const authApi = useApi(provider.apiRef); + const configApi = useApi(configApiRef); + + const [retry, setRetry] = useState<{} | boolean | undefined>(auto); + const [error, setError] = useState(); + + useEffect(() => { + const login = async () => { + const identity = await authApi.getBackstageIdentity({ + instantPopup: true, + }); + + const profile = await authApi.getProfile(); + onResult({ + userId: identity!.id, + profile: profile!, + getIdToken: () => { + return authApi.getBackstageIdentity().then(i => i!.idToken); + }, + signOut: async () => { + await authApi.signOut(); + }, + }); + }; + + if (retry) { + login().catch(setError); + } + }, [onResult, authApi, retry]); + + return ( + +
+ + + + setRetry({})} + > + Sign In + + } + > + {provider.message} + {error && error.name !== 'PopupRejectedError' && ( + + {error.message} + + )} + + + + + + ); +}; + +export const SignInPage = (props: Props) => { + if ('provider' in props) { + return ; + } + + return ; +}; From 23381386d6ee7a659ca24fc166356f70ae8c6a00 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 Jan 2021 19:25:58 +0100 Subject: [PATCH 19/99] docs: update auth docs to mention and prefer single-provider `SignInPage` --- docs/auth/index.md | 3 ++- docs/tutorials/quickstart-app-auth.md | 28 ++++++++++++--------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/docs/auth/index.md b/docs/auth/index.md index cf468c7bb7..a8ddf17c6b 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -86,7 +86,8 @@ to a `guest` identity for all users, without any ID token. To enable sign-in, a `SignInPage` needs to be configured, which in turn has to supply a user to the app. The `@backstage/core` package provides a basic sign-in page that allows both the user and the app developer to choose between a couple of different -sign-in methods. +sign-in methods, or to designate a single provider that may also be logged in to +automatically. ## Further Reading diff --git a/docs/tutorials/quickstart-app-auth.md b/docs/tutorials/quickstart-app-auth.md index 1c661d274c..d322e2ec3c 100644 --- a/docs/tutorials/quickstart-app-auth.md +++ b/docs/tutorials/quickstart-app-auth.md @@ -349,22 +349,18 @@ const app = createApp({ apis, plugins: Object.values(plugins), components: { - SignInPage: props => { - return ( - - ); - }, + SignInPage: props => ( + + ), }, }); ``` From da9f53c605d6850daa88d53b22256c4a2987e04e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 Jan 2021 19:31:31 +0100 Subject: [PATCH 20/99] add auto SignInPage changeset --- .changeset/ninety-keys-serve.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ninety-keys-serve.md diff --git a/.changeset/ninety-keys-serve.md b/.changeset/ninety-keys-serve.md new file mode 100644 index 0000000000..d15884b1c4 --- /dev/null +++ b/.changeset/ninety-keys-serve.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Add a `prop` union for `SignInPage` that allows it to be used for just a single provider, with inline errors, and optionally with automatic sign-in. From c777df180afc3a6bede6d7d7febc0dea1902b396 Mon Sep 17 00:00:00 2001 From: vitorgrenzel Date: Wed, 13 Jan 2021 09:20:58 -0300 Subject: [PATCH 21/99] feat(techdocs-common): add Azure Storage --- .changeset/chilly-dodos-drop.md | 6 + app-config.yaml | 2 +- docs/features/techdocs/README.md | 12 +- docs/features/techdocs/configuration.md | 13 + docs/features/techdocs/using-cloud-storage.md | 61 +++++ .../__mocks__/@azure/storage-blob.ts | 86 +++++++ packages/techdocs-common/package.json | 1 + .../src/stages/publish/azureStorage.test.ts | 149 ++++++++++++ .../src/stages/publish/azureStorage.ts | 223 ++++++++++++++++++ .../src/stages/publish/publish.test.ts | 25 ++ .../src/stages/publish/publish.ts | 4 + .../src/stages/publish/types.ts | 2 +- .../techdocs-backend/src/service/router.ts | 1 + plugins/techdocs/config.d.ts | 39 +++ 14 files changed, 616 insertions(+), 8 deletions(-) create mode 100644 .changeset/chilly-dodos-drop.md create mode 100644 packages/techdocs-common/__mocks__/@azure/storage-blob.ts create mode 100644 packages/techdocs-common/src/stages/publish/azureStorage.test.ts create mode 100644 packages/techdocs-common/src/stages/publish/azureStorage.ts diff --git a/.changeset/chilly-dodos-drop.md b/.changeset/chilly-dodos-drop.md new file mode 100644 index 0000000000..b262c14e4d --- /dev/null +++ b/.changeset/chilly-dodos-drop.md @@ -0,0 +1,6 @@ +--- +'@backstage/techdocs-common': patch +'@backstage/plugin-techdocs-backend': patch +--- + +1. Added option to use Azure Storage as a choice to store the static generated files for TechDocs. diff --git a/app-config.yaml b/app-config.yaml index c150ef5902..09d2e75253 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -80,7 +80,7 @@ techdocs: generators: techdocs: 'docker' # Alternatives - 'local' publisher: - type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives. + type: 'local' # Alternatives - 'googleGcs' or 'awsS3' or 'azureStorage'. Read documentation for using alternatives. sentry: organization: my-company diff --git a/docs/features/techdocs/README.md b/docs/features/techdocs/README.md index c43d6d978d..24b4410092 100644 --- a/docs/features/techdocs/README.md +++ b/docs/features/techdocs/README.md @@ -108,12 +108,12 @@ providers are used. | GitLab | Yes ✅ | | GitLab Enterprise | Yes ✅ | -| File Storage Provider | Support Status | -| --------------------------------- | ----------------------------------------------------------------- | -| Local Filesystem of Backstage app | Yes ✅ | -| Google Cloud Storage (GCS) | Yes ✅ | -| Amazon Web Services (AWS) S3 | Yes ✅ | -| Azure Storage | No ❌ [#3938](https://github.com/backstage/backstage/issues/3938) | +| File Storage Provider | Support Status | +| --------------------------------- | -------------- | +| Local Filesystem of Backstage app | Yes ✅ | +| Google Cloud Storage (GCS) | Yes ✅ | +| Amazon Web Services (AWS) S3 | Yes ✅ | +| Azure Storage | Yes ✅ | [Reach out to us](#feedback) if you want to request more platforms. diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index 1580abe69e..8d5906b18f 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -84,4 +84,17 @@ techdocs: # https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-region.html region: $env: AWS_REGION + + # Required when techdocs.publisher.type is set to 'azureStorage'. Skip otherwise. + + azureStorage: + # An API key is required to write to a storage container. + credentials: + account: + $env: TECHDOCS_AZURE_STORAGE_ACCOUNT + accountKey: + $env: TECHDOCS_AZURE_STORAGE_ACCOUNT_KEY + + # Azure Storage Container Name + containerName: 'techdocs-storage' ``` diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index 21206dede5..2d906ca1b9 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -195,3 +195,64 @@ Your Backstage app is now ready to use AWS S3 for TechDocs, to store and read the static generated documentation files. When you start the backend of the app, you should be able to see `techdocs info Successfully connected to the AWS S3 bucket` in the logs. + +## Configuring Azure Storage Container with TechDocs + +Follow the +[official Azure Storage documentation](https://docs.microsoft.com/pt-br/javascript/api/@azure/storage-blob/?view=azure-node-latest) +for the latest instructions on the following steps involving Azure Storage. + +**1. Set `techdocs.publisher.type` config in your `app-config.yaml`** + +Set `techdocs.publisher.type` to `'azureStorage'`. + +```yaml +techdocs: + publisher: + type: 'azureStorage' +``` + +**2. Service account credentials** + +To get credentials, access the Azure Portal and go to "Settings > Access Keys", +and get your Storage account name and Primary Key. + +```yaml +techdocs: + publisher: + type: 'azureStorage' + azureStorage: + credentials: + account: 'account' + accountKey: 'accountKey' +``` + +**3. Azure Storage Container** + +Create a dedicated container for TechDocs sites. techdocs-backend will publish +documentation to this container. TechDocs will fetch files from here to serve +documentation in Backstage. + +To create a new container, access "Blob Service > Containers > New Container". + +Set the name of the container to +`techdocs.publisher.azureStorage.containerName`. + +```yaml +techdocs: + publisher: + type: 'azureStorage' + azureStorage: + credentials: + account: 'account' + accountKey: 'accountKey' + containerName: 'name-of-techdocs-storage-container' +``` + +**4. That's it!** + +Your Backstage app is now ready to use Azure Storage for TechDocs, to store and +read the static generated documentation files. When you start the backend of the +app, you should be able to see +`techdocs info Successfully connected to the Azure Storage container` in the +logs. diff --git a/packages/techdocs-common/__mocks__/@azure/storage-blob.ts b/packages/techdocs-common/__mocks__/@azure/storage-blob.ts new file mode 100644 index 0000000000..60a705f6a7 --- /dev/null +++ b/packages/techdocs-common/__mocks__/@azure/storage-blob.ts @@ -0,0 +1,86 @@ +/* + * 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 fs from 'fs'; + +export class BlockBlobClient { + private readonly blobName; + + constructor(blobName: string) { + this.blobName = blobName; + } + + uploadFile(source: string) { + return new Promise((resolve, reject) => { + if (!fs.existsSync(source)) { + reject(''); + } else { + resolve(''); + } + }); + } + + exists() { + return new Promise((resolve, reject) => { + if (fs.existsSync(this.blobName)) { + resolve(true); + } else { + reject({ message: 'The object doest not exist !' }); + } + }); + } +} + +export class ContainerClient { + private readonly containerName; + + constructor(containerName: string) { + this.containerName = containerName; + } + + getProperties() { + return new Promise(resolve => { + resolve(''); + }); + } + + getBlockBlobClient(blobName: string) { + return new BlockBlobClient(blobName); + } +} + +export class BlobServiceClient { + private readonly url; + private readonly credential; + + constructor(url: string, credential?: StorageSharedKeyCredential) { + this.url = url; + this.credential = credential; + } + + getContainerClient(containerName: string) { + return new ContainerClient(containerName); + } +} + +export class StorageSharedKeyCredential { + private readonly accountName; + private readonly accountKey; + + constructor(accountName: string, accountKey: string) { + this.accountName = accountName; + this.accountKey = accountKey; + } +} diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 64c5c19425..09893bd38e 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -36,6 +36,7 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { + "@azure/storage-blob": "^12.3.0", "@aws-sdk/client-s3": "^3.1.0", "@backstage/backend-common": "^0.5.1", "@backstage/catalog-model": "^0.7.0", diff --git a/packages/techdocs-common/src/stages/publish/azureStorage.test.ts b/packages/techdocs-common/src/stages/publish/azureStorage.test.ts new file mode 100644 index 0000000000..05b5441bff --- /dev/null +++ b/packages/techdocs-common/src/stages/publish/azureStorage.test.ts @@ -0,0 +1,149 @@ +/* + * 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 mockFs from 'mock-fs'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '@backstage/backend-common'; +import { AzureStoragePublish } from './azureStorage'; +import { PublisherBase } from './types'; +import type { Entity } from '@backstage/catalog-model'; + +const createMockEntity = (annotations = {}) => { + return { + apiVersion: 'version', + kind: 'TestKind', + metadata: { + name: 'test-component-name', + namespace: 'test-namespace', + annotations: { + ...annotations, + }, + }, + }; +}; + +const getEntityRootDir = (entity: Entity) => { + const { + kind, + metadata: { namespace, name }, + } = entity; + const entityRootDir = `${namespace}/${kind}/${name}`; + return entityRootDir; +}; + +const logger = getVoidLogger(); +jest.spyOn(logger, 'info').mockReturnValue(logger); +jest.spyOn(logger, 'error').mockReturnValue(logger); + +let publisher: PublisherBase; + +beforeEach(async () => { + const mockConfig = new ConfigReader({ + techdocs: { + requestUrl: 'http://localhost:7000', + publisher: { + type: 'azureStorage', + azureStorage: { + credentials: { + account: 'account', + accountKey: 'accountKey', + }, + containerName: 'containerName', + }, + }, + }, + }); + + publisher = await AzureStoragePublish.fromConfig(mockConfig, logger); +}); + +describe('AzureStoragePublish', () => { + describe('publish', () => { + it('should publish a directory', async () => { + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + mockFs({ + [entityRootDir]: { + 'index.html': '', + '404.html': '', + assets: { + 'main.css': '', + }, + }, + }); + + expect( + await publisher.publish({ + entity, + directory: entityRootDir, + }), + ).toBeUndefined(); + mockFs.restore(); + }); + + it('should fail to publish a directory', async () => { + const wrongPathToGeneratedDirectory = 'wrong/path/to/generatedDirectory'; + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + mockFs({ + [entityRootDir]: { + 'index.html': '', + '404.html': '', + assets: { + 'main.css': '', + }, + }, + }); + + await publisher + .publish({ + entity, + directory: wrongPathToGeneratedDirectory, + }) + .catch(error => + expect(error).toEqual( + new Error( + `Unable to upload file(s) to Azure Storage. Error Failed to read template directory: ENOENT, no such file or directory '${wrongPathToGeneratedDirectory}'`, + ), + ), + ); + mockFs.restore(); + }); + }); + + describe('hasDocsBeenGenerated', () => { + it('should return true if docs has been generated', async () => { + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + mockFs({ + [entityRootDir]: { + 'index.html': 'file-content', + }, + }); + + expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true); + mockFs.restore(); + }); + + it('should return false if docs has not been generated', async () => { + const entity = createMockEntity(); + + expect(await publisher.hasDocsBeenGenerated(entity)).toBe(false); + }); + }); +}); diff --git a/packages/techdocs-common/src/stages/publish/azureStorage.ts b/packages/techdocs-common/src/stages/publish/azureStorage.ts new file mode 100644 index 0000000000..bc9a6825d9 --- /dev/null +++ b/packages/techdocs-common/src/stages/publish/azureStorage.ts @@ -0,0 +1,223 @@ +/* + * 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 path from 'path'; +import express from 'express'; +import { + BlobServiceClient, + BlobUploadCommonResponse, + StorageSharedKeyCredential, +} from '@azure/storage-blob'; +import { Logger } from 'winston'; +import { Entity, EntityName } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { getHeadersForFileExtension, getFileTreeRecursively } from './helpers'; +import { PublisherBase, PublishRequest } from './types'; + +export class AzureStoragePublish implements PublisherBase { + static async fromConfig( + config: Config, + logger: Logger, + ): Promise { + let account = ''; + let accountKey = ''; + let containerName = ''; + try { + account = config.getString( + 'techdocs.publisher.azureStorage.credentials.account', + ); + accountKey = config.getString( + 'techdocs.publisher.azureStorage.credentials.accountKey', + ); + containerName = config.getString( + 'techdocs.publisher.azureStorage.containerName', + ); + } catch (error) { + throw new Error( + "Since techdocs.publisher.type is set to 'azureStorage' in your app config, " + + 'credentials and containerName are required in techdocs.publisher.azureStorage ' + + 'required to authenticate with Azure Storage.', + ); + } + + const credential = new StorageSharedKeyCredential(account, accountKey); + const storageClient = new BlobServiceClient( + `https://${account}.blob.core.windows.net`, + credential, + ); + + await storageClient + .getContainerClient(containerName) + .getProperties() + .then(() => { + logger.info( + `Successfully connected to the Azure Storage container ${containerName}.`, + ); + }) + .catch(reason => { + logger.error( + `Could not retrieve metadata about the Azure Storage container ${containerName}. ` + + 'Make sure the Azure project and the container exists and the access key located at the path ' + + "techdocs.publisher.azureStorage.credentials defined in app config has the role 'Storage Object Creator'. " + + 'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage', + ); + throw new Error(`from Azure Storage client library: ${reason.message}`); + }); + + return new AzureStoragePublish(storageClient, containerName, logger); + } + + constructor( + private readonly storageClient: BlobServiceClient, + private readonly containerName: string, + private readonly logger: Logger, + ) { + this.storageClient = storageClient; + this.containerName = containerName; + this.logger = logger; + } + + /** + * Upload all the files from the generated `directory` to the Azure Storage container. + * Directory structure used in the container is - entityNamespace/entityKind/entityName/index.html + */ + async publish({ entity, directory }: PublishRequest): Promise { + try { + // Note: Azure Storage manages creation of parent directories if they do not exist. + // So collecting path of only the files is good enough. + const allFilesToUpload = await getFileTreeRecursively(directory); + + const uploadPromises: Array> = []; + allFilesToUpload.forEach(filePath => { + // Remove the absolute path prefix of the source directory + // Path of all files to upload, relative to the root of the source directory + // e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png'] + const relativeFilePath = filePath.replace(`${directory}/`, ''); + const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; + const destination = path.normalize( + `${entityRootDir}/${relativeFilePath}`, + ); // Azure Storage Container file relative path + // TODO: Upload in chunks of ~10 files instead of all files at once. + uploadPromises.push( + this.storageClient + .getContainerClient(this.containerName) + .getBlockBlobClient(destination) + .uploadFile(filePath), + ); + }); + + await Promise.all(uploadPromises).then(() => { + this.logger.info( + `Successfully uploaded all the generated files for Entity ${entity.metadata.name}. Total number of files: ${allFilesToUpload.length}`, + ); + }); + return; + } catch (e) { + const errorMessage = `Unable to upload file(s) to Azure Storage. Error ${e.message}`; + this.logger.error(errorMessage); + throw new Error(errorMessage); + } + } + + download(containerName: string, path: string): Promise { + return new Promise((resolve, reject) => { + const fileStreamChunks: Array = []; + this.storageClient + .getContainerClient(containerName) + .getBlockBlobClient(path) + .download() + .then(res => { + const body = res.readableStreamBody; + if (!body) { + reject(new Error(`Unable to parse the response data`)); + return; + } + body + .on('error', e => { + this.logger.error(e.message); + reject(e.message); + }) + .on('data', chunk => { + fileStreamChunks.push(chunk); + }) + .on('end', () => { + resolve(Buffer.concat(fileStreamChunks).toString()); + }); + }); + }); + } + + async fetchTechDocsMetadata(entityName: EntityName): Promise { + const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; + try { + return this.download( + this.containerName, + `${entityRootDir}/techdocs_metadata.json`, + ); + } catch (e) { + this.logger.error(e.message); + throw e; + } + } + + /** + * Express route middleware to serve static files on a route in techdocs-backend. + */ + docsRouter(): express.Handler { + return (req, res) => { + // Trim the leading forward slash + // filePath example - /default/Component/documented-component/index.html + const filePath = req.path.replace(/^\//, ''); + // Files with different extensions (CSS, HTML) need to be served with different headers + const fileExtension = path.extname(filePath); + const responseHeaders = getHeadersForFileExtension(fileExtension); + + try { + this.download(this.containerName, filePath).then(fileContent => { + // Inject response headers + for (const [headerKey, headerValue] of Object.entries( + responseHeaders, + )) { + res.setHeader(headerKey, headerValue); + } + res.send(fileContent); + }); + } catch (e) { + this.logger.error(e.message); + res.status(404).send(e.message); + } + }; + } + + /** + * A helper function which checks if index.html of an Entity's docs site is available. This + * can be used to verify if there are any pre-generated docs available to serve. + */ + async hasDocsBeenGenerated(entity: Entity): Promise { + return new Promise(resolve => { + const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; + this.storageClient + .getContainerClient(this.containerName) + .getBlockBlobClient(`${entityRootDir}/index.html`) + .exists() + .then((response: boolean) => { + resolve(response); + }) + .catch(() => { + resolve(false); + }); + }); + } +} diff --git a/packages/techdocs-common/src/stages/publish/publish.test.ts b/packages/techdocs-common/src/stages/publish/publish.test.ts index 89f2c0ffdd..a97eadb404 100644 --- a/packages/techdocs-common/src/stages/publish/publish.test.ts +++ b/packages/techdocs-common/src/stages/publish/publish.test.ts @@ -22,6 +22,7 @@ import { Publisher } from './publish'; import { LocalPublish } from './local'; import { GoogleGCSPublish } from './googleStorage'; import { AwsS3Publish } from './awsS3'; +import { AzureStoragePublish } from './azureStorage'; const logger = getVoidLogger(); const discovery: jest.Mocked = { @@ -105,4 +106,28 @@ describe('Publisher', () => { }); expect(publisher).toBeInstanceOf(AwsS3Publish); }); + + it('should create Azure Storage publisher from config', async () => { + const mockConfig = new ConfigReader({ + techdocs: { + requestUrl: 'http://localhost:7000', + publisher: { + type: 'azureStorage', + azureStorage: { + credentials: { + account: 'account', + accountKey: 'accountKey', + }, + containerName: 'containerName', + }, + }, + }, + }); + + const publisher = await Publisher.fromConfig(mockConfig, { + logger, + discovery, + }); + expect(publisher).toBeInstanceOf(AzureStoragePublish); + }); }); diff --git a/packages/techdocs-common/src/stages/publish/publish.ts b/packages/techdocs-common/src/stages/publish/publish.ts index 82232c2fd1..fc72d2812e 100644 --- a/packages/techdocs-common/src/stages/publish/publish.ts +++ b/packages/techdocs-common/src/stages/publish/publish.ts @@ -21,6 +21,7 @@ import { PublisherType, PublisherBase } from './types'; import { LocalPublish } from './local'; import { GoogleGCSPublish } from './googleStorage'; import { AwsS3Publish } from './awsS3'; +import { AzureStoragePublish } from './azureStorage'; type factoryOptions = { logger: Logger; @@ -47,6 +48,9 @@ export class Publisher { case 'awsS3': logger.info('Creating AWS S3 Bucket publisher for TechDocs'); return AwsS3Publish.fromConfig(config, logger); + case 'azureStorage': + logger.info('Creating Azure Storage Container publisher for TechDocs'); + return AzureStoragePublish.fromConfig(config, logger); case 'local': logger.info('Creating Local publisher for TechDocs'); return new LocalPublish(config, logger, discovery); diff --git a/packages/techdocs-common/src/stages/publish/types.ts b/packages/techdocs-common/src/stages/publish/types.ts index db6a075d43..f67f65ee23 100644 --- a/packages/techdocs-common/src/stages/publish/types.ts +++ b/packages/techdocs-common/src/stages/publish/types.ts @@ -19,7 +19,7 @@ import express from 'express'; /** * Key for all the different types of TechDocs publishers that are supported. */ -export type PublisherType = 'local' | 'googleGcs' | 'awsS3'; +export type PublisherType = 'local' | 'googleGcs' | 'awsS3' | 'azureStorage'; export type PublishRequest = { entity: Entity; diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index 5ad23cde84..cbb6efa3dd 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -148,6 +148,7 @@ export async function createRouter({ } break; case 'awsS3': + case 'azureStorage': case 'googleGcs': // This block should be valid for all external storage implementations. So no need to duplicate in future, // add the publisher type in the list here. diff --git a/plugins/techdocs/config.d.ts b/plugins/techdocs/config.d.ts index f9831f27cb..1dd7319f61 100644 --- a/plugins/techdocs/config.d.ts +++ b/plugins/techdocs/config.d.ts @@ -114,6 +114,45 @@ export interface Config { region?: string; }; } + | { + /** + * attr: 'type' - accepts a string value + * e.g. type: 'azureStorage' + * alternatives: 'azureStorage' etc. + * @see http://backstage.io/docs/features/techdocs/configuration + */ + type: 'azureStorage'; + + /** + * azureStorage required when 'type' is set to azureStorage + */ + azureStorage?: { + /** + * Credentials used to access a storage container + * @visibility secret + */ + credentials: { + /** + * Account access name + * attr: 'account' - accepts a string value + * @visibility secret + */ + account: string; + /** + * Account secret primary key + * attr: 'accountKey' - accepts a string value + * @visibility secret + */ + accountKey: string; + }; + /** + * Cloud Storage Container Name + * attr: 'containerName' - accepts a string value + * @visibility backend + */ + containerName: string; + }; + } | { /** * attr: 'type' - accepts a string value From 42494c7e91650459cd1fe5158b5ee9ec38bcd0ba Mon Sep 17 00:00:00 2001 From: vitorgrenzel Date: Wed, 20 Jan 2021 17:21:07 -0300 Subject: [PATCH 22/99] feat(docs): add Azure Blob Storage --- .changeset/chilly-dodos-drop.md | 2 +- docs/features/techdocs/README.md | 2 +- docs/features/techdocs/configuration.md | 20 +++++----- docs/features/techdocs/using-cloud-storage.md | 40 ++++++++++--------- 4 files changed, 34 insertions(+), 30 deletions(-) diff --git a/.changeset/chilly-dodos-drop.md b/.changeset/chilly-dodos-drop.md index b262c14e4d..2af5255e70 100644 --- a/.changeset/chilly-dodos-drop.md +++ b/.changeset/chilly-dodos-drop.md @@ -3,4 +3,4 @@ '@backstage/plugin-techdocs-backend': patch --- -1. Added option to use Azure Storage as a choice to store the static generated files for TechDocs. +1. Added option to use Azure Blob Storage as a choice to store the static generated files for TechDocs. diff --git a/docs/features/techdocs/README.md b/docs/features/techdocs/README.md index 24b4410092..f81f41668d 100644 --- a/docs/features/techdocs/README.md +++ b/docs/features/techdocs/README.md @@ -113,7 +113,7 @@ providers are used. | Local Filesystem of Backstage app | Yes ✅ | | Google Cloud Storage (GCS) | Yes ✅ | | Amazon Web Services (AWS) S3 | Yes ✅ | -| Azure Storage | Yes ✅ | +| Azure Blob Storage | Yes ✅ | [Reach out to us](#feedback) if you want to request more platforms. diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index 8d5906b18f..efed37fc69 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -44,7 +44,7 @@ techdocs: # or you want to use External storage providers like Google Cloud Storage, AWS S3, etc. publisher: - # techdocs.publisher.type can be - 'local' or 'googleGcs' or 'awsS3' (azureStorage to be available in future). + # techdocs.publisher.type can be - 'local' or 'googleGcs' or 'awsS3' or 'azureBlobStorage'. # When set to 'local', techdocs-backend will create a 'static' directory at its root to store generated documentation files. # When set to 'googleGcs', techdocs-backend will use a Google Cloud Storage Bucket to store generated documentation files. # When set to 'awsS3', techdocs-backend will use an Amazon Web Service (AWS) S3 bucket to store generated documentation files. @@ -85,16 +85,18 @@ techdocs: region: $env: AWS_REGION - # Required when techdocs.publisher.type is set to 'azureStorage'. Skip otherwise. + # Required when techdocs.publisher.type is set to 'azureBlobStorage'. Skip otherwise. - azureStorage: - # An API key is required to write to a storage container. + azureBlobStorage: + # (Required) Azure Blob Storage Container Name + containerName: 'techdocs-storage' + + # (Optional) An API key is required to write to a storage container. + # If not set, environment variables will be used to authenticate. + #https://docs.microsoft.com/en-us/azure/storage/common/storage-auth?toc=/azure/storage/blobs/toc.json credentials: account: - $env: TECHDOCS_AZURE_STORAGE_ACCOUNT + $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT accountKey: - $env: TECHDOCS_AZURE_STORAGE_ACCOUNT_KEY - - # Azure Storage Container Name - containerName: 'techdocs-storage' + $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_KEY ``` diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index 2d906ca1b9..9ae14abfe3 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -196,20 +196,20 @@ the static generated documentation files. When you start the backend of the app, you should be able to see `techdocs info Successfully connected to the AWS S3 bucket` in the logs. -## Configuring Azure Storage Container with TechDocs +## Configuring Azure Blob Storage Container with TechDocs Follow the -[official Azure Storage documentation](https://docs.microsoft.com/pt-br/javascript/api/@azure/storage-blob/?view=azure-node-latest) -for the latest instructions on the following steps involving Azure Storage. +[official Azure Blob Storage documentation](https://docs.microsoft.com/en-us/azure/storage/common/storage-auth?toc=/azure/storage/blobs/toc.json) +for the latest instructions on the following steps involving Azure Blob Storage. **1. Set `techdocs.publisher.type` config in your `app-config.yaml`** -Set `techdocs.publisher.type` to `'azureStorage'`. +Set `techdocs.publisher.type` to `'azureBlobStorage'`. ```yaml techdocs: publisher: - type: 'azureStorage' + type: 'azureBlobStorage' ``` **2. Service account credentials** @@ -220,14 +220,14 @@ and get your Storage account name and Primary Key. ```yaml techdocs: publisher: - type: 'azureStorage' - azureStorage: + type: 'azureBlobStorage' + azureBlobStorage: credentials: account: 'account' accountKey: 'accountKey' ``` -**3. Azure Storage Container** +**3. Azure Blob Storage Container** Create a dedicated container for TechDocs sites. techdocs-backend will publish documentation to this container. TechDocs will fetch files from here to serve @@ -236,23 +236,25 @@ documentation in Backstage. To create a new container, access "Blob Service > Containers > New Container". Set the name of the container to -`techdocs.publisher.azureStorage.containerName`. +`techdocs.publisher.azureBlobStorage.containerName`. ```yaml techdocs: publisher: - type: 'azureStorage' - azureStorage: - credentials: - account: 'account' - accountKey: 'accountKey' + type: 'azureBlobStorage' + azureBlobStorage: containerName: 'name-of-techdocs-storage-container' + credentials: + account: + $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT + accountKey: + $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_KEY ``` **4. That's it!** -Your Backstage app is now ready to use Azure Storage for TechDocs, to store and -read the static generated documentation files. When you start the backend of the -app, you should be able to see -`techdocs info Successfully connected to the Azure Storage container` in the -logs. +Your Backstage app is now ready to use Azure Blob Storage for TechDocs, to store +and read the static generated documentation files. When you start the backend of +the app, you should be able to see +`techdocs info Successfully connected to the Azure Blob Storage container` in +the logs. From 59b8d5a0d94110b5d4c7ed9cc89076170e72a817 Mon Sep 17 00:00:00 2001 From: vitorgrenzel Date: Wed, 20 Jan 2021 17:21:40 -0300 Subject: [PATCH 23/99] feat(techdocs-common): add Azure Blob Storage --- app-config.yaml | 2 +- ...orage.test.ts => azureBlobStorage.test.ts} | 12 +- .../{azureStorage.ts => azureBlobStorage.ts} | 103 +++++++++++------- .../src/stages/publish/publish.test.ts | 10 +- .../src/stages/publish/publish.ts | 10 +- .../src/stages/publish/types.ts | 6 +- .../techdocs-backend/src/service/router.ts | 2 +- plugins/techdocs/config.d.ts | 18 +-- 8 files changed, 99 insertions(+), 64 deletions(-) rename packages/techdocs-common/src/stages/publish/{azureStorage.test.ts => azureBlobStorage.test.ts} (89%) rename packages/techdocs-common/src/stages/publish/{azureStorage.ts => azureBlobStorage.ts} (67%) diff --git a/app-config.yaml b/app-config.yaml index 09d2e75253..90cf73f58f 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -80,7 +80,7 @@ techdocs: generators: techdocs: 'docker' # Alternatives - 'local' publisher: - type: 'local' # Alternatives - 'googleGcs' or 'awsS3' or 'azureStorage'. Read documentation for using alternatives. + type: 'local' # Alternatives - 'googleGcs' or 'awsS3' or 'azureBlobStorage'. Read documentation for using alternatives. sentry: organization: my-company diff --git a/packages/techdocs-common/src/stages/publish/azureStorage.test.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts similarity index 89% rename from packages/techdocs-common/src/stages/publish/azureStorage.test.ts rename to packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts index 05b5441bff..95e4a52f62 100644 --- a/packages/techdocs-common/src/stages/publish/azureStorage.test.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts @@ -16,7 +16,7 @@ import mockFs from 'mock-fs'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; -import { AzureStoragePublish } from './azureStorage'; +import { AzureBlobStoragePublish } from './azureBlobStorage'; import { PublisherBase } from './types'; import type { Entity } from '@backstage/catalog-model'; @@ -54,8 +54,8 @@ beforeEach(async () => { techdocs: { requestUrl: 'http://localhost:7000', publisher: { - type: 'azureStorage', - azureStorage: { + type: 'azureBlobStorage', + azureBlobStorage: { credentials: { account: 'account', accountKey: 'accountKey', @@ -66,10 +66,10 @@ beforeEach(async () => { }, }); - publisher = await AzureStoragePublish.fromConfig(mockConfig, logger); + publisher = await AzureBlobStoragePublish.fromConfig(mockConfig, logger); }); -describe('AzureStoragePublish', () => { +describe('AzureBlobStoragePublish', () => { describe('publish', () => { it('should publish a directory', async () => { const entity = createMockEntity(); @@ -117,7 +117,7 @@ describe('AzureStoragePublish', () => { .catch(error => expect(error).toEqual( new Error( - `Unable to upload file(s) to Azure Storage. Error Failed to read template directory: ENOENT, no such file or directory '${wrongPathToGeneratedDirectory}'`, + `Unable to upload file(s) to Azure Blob Storage. Error Failed to read template directory: ENOENT, no such file or directory '${wrongPathToGeneratedDirectory}'`, ), ), ); diff --git a/packages/techdocs-common/src/stages/publish/azureStorage.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts similarity index 67% rename from packages/techdocs-common/src/stages/publish/azureStorage.ts rename to packages/techdocs-common/src/stages/publish/azureBlobStorage.ts index bc9a6825d9..7ba4d35892 100644 --- a/packages/techdocs-common/src/stages/publish/azureStorage.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts @@ -24,34 +24,45 @@ import { Logger } from 'winston'; import { Entity, EntityName } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { getHeadersForFileExtension, getFileTreeRecursively } from './helpers'; -import { PublisherBase, PublishRequest } from './types'; +import { PublisherBase, PublishRequest, TechDocsMetadata } from './types'; +import limiterFactory from 'p-limit'; +import JSON5 from 'json5'; -export class AzureStoragePublish implements PublisherBase { +// The number of batches that may be ongoing at the same time. +const BATCH_CONCURRENCY = 3; + +export class AzureBlobStoragePublish implements PublisherBase { static async fromConfig( config: Config, logger: Logger, ): Promise { - let account = ''; - let accountKey = ''; let containerName = ''; try { - account = config.getString( - 'techdocs.publisher.azureStorage.credentials.account', - ); - accountKey = config.getString( - 'techdocs.publisher.azureStorage.credentials.accountKey', - ); containerName = config.getString( - 'techdocs.publisher.azureStorage.containerName', + 'techdocs.publisher.azureBlobStorage.containerName', ); } catch (error) { throw new Error( - "Since techdocs.publisher.type is set to 'azureStorage' in your app config, " + - 'credentials and containerName are required in techdocs.publisher.azureStorage ' + - 'required to authenticate with Azure Storage.', + "Since techdocs.publisher.type is set to 'awsS3' in your app config, " + + 'techdocs.publisher.awsS3.bucketName is required.', ); } + // Credentials is an optional config. If missing, default AWS environment variables + // or AWS shared credentials file at ~/.aws/credentials will be used to authenticate + // https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html + // https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-shared.html + let account = ''; + let accountKey = ''; + account = + config.getOptionalString( + 'techdocs.publisher.azureBlobStorage.credentials.account', + ) || ''; + accountKey = + config.getOptionalString( + 'techdocs.publisher.azureBlobStorage.credentials.accountKey', + ) || ''; + const credential = new StorageSharedKeyCredential(account, accountKey); const storageClient = new BlobServiceClient( `https://${account}.blob.core.windows.net`, @@ -63,20 +74,22 @@ export class AzureStoragePublish implements PublisherBase { .getProperties() .then(() => { logger.info( - `Successfully connected to the Azure Storage container ${containerName}.`, + `Successfully connected to the Azure Blob Storage container ${containerName}.`, ); }) .catch(reason => { logger.error( - `Could not retrieve metadata about the Azure Storage container ${containerName}. ` + + `Could not retrieve metadata about the Azure Blob Storage container ${containerName}. ` + 'Make sure the Azure project and the container exists and the access key located at the path ' + - "techdocs.publisher.azureStorage.credentials defined in app config has the role 'Storage Object Creator'. " + + "techdocs.publisher.azureBlobStorage.credentials defined in app config has the role 'Storage Object Creator'. " + 'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage', ); - throw new Error(`from Azure Storage client library: ${reason.message}`); + throw new Error( + `from Azure Blob Storage client library: ${reason.message}`, + ); }); - return new AzureStoragePublish(storageClient, containerName, logger); + return new AzureBlobStoragePublish(storageClient, containerName, logger); } constructor( @@ -90,17 +103,23 @@ export class AzureStoragePublish implements PublisherBase { } /** - * Upload all the files from the generated `directory` to the Azure Storage container. + * Upload all the files from the generated `directory` to the Azure Blob Storage container. * Directory structure used in the container is - entityNamespace/entityKind/entityName/index.html */ async publish({ entity, directory }: PublishRequest): Promise { try { - // Note: Azure Storage manages creation of parent directories if they do not exist. + // Note: Azure Blob Storage manages creation of parent directories if they do not exist. // So collecting path of only the files is good enough. const allFilesToUpload = await getFileTreeRecursively(directory); const uploadPromises: Array> = []; - allFilesToUpload.forEach(filePath => { + + // Bound the number of concurrent batches. We want a bit of concurrency for + // performance reasons, but not so much that we starve the connection pool + // or start thrashing. + const limiter = limiterFactory(BATCH_CONCURRENCY); + + const promises = allFilesToUpload.map(filePath => { // Remove the absolute path prefix of the source directory // Path of all files to upload, relative to the root of the source directory // e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png'] @@ -108,30 +127,33 @@ export class AzureStoragePublish implements PublisherBase { const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; const destination = path.normalize( `${entityRootDir}/${relativeFilePath}`, - ); // Azure Storage Container file relative path + ); // Azure Blob Storage Container file relative path + // TODO: Upload in chunks of ~10 files instead of all files at once. - uploadPromises.push( - this.storageClient - .getContainerClient(this.containerName) - .getBlockBlobClient(destination) - .uploadFile(filePath), - ); + return limiter(async () => { + await uploadPromises.push( + this.storageClient + .getContainerClient(this.containerName) + .getBlockBlobClient(destination) + .uploadFile(filePath), + ); + }); }); - await Promise.all(uploadPromises).then(() => { + await Promise.all(promises).then(() => { this.logger.info( `Successfully uploaded all the generated files for Entity ${entity.metadata.name}. Total number of files: ${allFilesToUpload.length}`, ); }); return; } catch (e) { - const errorMessage = `Unable to upload file(s) to Azure Storage. Error ${e.message}`; + const errorMessage = `Unable to upload file(s) to Azure Blob Storage. Error ${e.message}`; this.logger.error(errorMessage); throw new Error(errorMessage); } } - download(containerName: string, path: string): Promise { + private download(containerName: string, path: string): Promise { return new Promise((resolve, reject) => { const fileStreamChunks: Array = []; this.storageClient @@ -153,19 +175,24 @@ export class AzureStoragePublish implements PublisherBase { fileStreamChunks.push(chunk); }) .on('end', () => { - resolve(Buffer.concat(fileStreamChunks).toString()); + resolve(Buffer.concat(fileStreamChunks)); }); }); }); } - async fetchTechDocsMetadata(entityName: EntityName): Promise { + async fetchTechDocsMetadata( + entityName: EntityName, + ): Promise { const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; try { - return this.download( - this.containerName, - `${entityRootDir}/techdocs_metadata.json`, - ); + return await new Promise(resolve => { + const download = this.download( + this.containerName, + `${entityRootDir}/techdocs_metadata.json`, + ); + resolve(JSON5.parse(download.toString())); + }); } catch (e) { this.logger.error(e.message); throw e; diff --git a/packages/techdocs-common/src/stages/publish/publish.test.ts b/packages/techdocs-common/src/stages/publish/publish.test.ts index a97eadb404..a563e53a9b 100644 --- a/packages/techdocs-common/src/stages/publish/publish.test.ts +++ b/packages/techdocs-common/src/stages/publish/publish.test.ts @@ -22,7 +22,7 @@ import { Publisher } from './publish'; import { LocalPublish } from './local'; import { GoogleGCSPublish } from './googleStorage'; import { AwsS3Publish } from './awsS3'; -import { AzureStoragePublish } from './azureStorage'; +import { AzureBlobStoragePublish } from './azureBlobStorage'; const logger = getVoidLogger(); const discovery: jest.Mocked = { @@ -107,13 +107,13 @@ describe('Publisher', () => { expect(publisher).toBeInstanceOf(AwsS3Publish); }); - it('should create Azure Storage publisher from config', async () => { + it('should create Azure Blob Storage publisher from config', async () => { const mockConfig = new ConfigReader({ techdocs: { requestUrl: 'http://localhost:7000', publisher: { - type: 'azureStorage', - azureStorage: { + type: 'azureBlobStorage', + azureBlobStorage: { credentials: { account: 'account', accountKey: 'accountKey', @@ -128,6 +128,6 @@ describe('Publisher', () => { logger, discovery, }); - expect(publisher).toBeInstanceOf(AzureStoragePublish); + expect(publisher).toBeInstanceOf(AzureBlobStoragePublish); }); }); diff --git a/packages/techdocs-common/src/stages/publish/publish.ts b/packages/techdocs-common/src/stages/publish/publish.ts index fc72d2812e..a4e33a2d9c 100644 --- a/packages/techdocs-common/src/stages/publish/publish.ts +++ b/packages/techdocs-common/src/stages/publish/publish.ts @@ -21,7 +21,7 @@ import { PublisherType, PublisherBase } from './types'; import { LocalPublish } from './local'; import { GoogleGCSPublish } from './googleStorage'; import { AwsS3Publish } from './awsS3'; -import { AzureStoragePublish } from './azureStorage'; +import { AzureBlobStoragePublish } from './azureBlobStorage'; type factoryOptions = { logger: Logger; @@ -48,9 +48,11 @@ export class Publisher { case 'awsS3': logger.info('Creating AWS S3 Bucket publisher for TechDocs'); return AwsS3Publish.fromConfig(config, logger); - case 'azureStorage': - logger.info('Creating Azure Storage Container publisher for TechDocs'); - return AzureStoragePublish.fromConfig(config, logger); + case 'azureBlobStorage': + logger.info( + 'Creating Azure Blob Storage Container publisher for TechDocs', + ); + return AzureBlobStoragePublish.fromConfig(config, logger); case 'local': logger.info('Creating Local publisher for TechDocs'); return new LocalPublish(config, logger, discovery); diff --git a/packages/techdocs-common/src/stages/publish/types.ts b/packages/techdocs-common/src/stages/publish/types.ts index f67f65ee23..5e953deb81 100644 --- a/packages/techdocs-common/src/stages/publish/types.ts +++ b/packages/techdocs-common/src/stages/publish/types.ts @@ -19,7 +19,11 @@ import express from 'express'; /** * Key for all the different types of TechDocs publishers that are supported. */ -export type PublisherType = 'local' | 'googleGcs' | 'awsS3' | 'azureStorage'; +export type PublisherType = + | 'local' + | 'googleGcs' + | 'awsS3' + | 'azureBlobStorage'; export type PublishRequest = { entity: Entity; diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index cbb6efa3dd..dc5e6a4911 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -148,7 +148,7 @@ export async function createRouter({ } break; case 'awsS3': - case 'azureStorage': + case 'azureBlobStorage': case 'googleGcs': // This block should be valid for all external storage implementations. So no need to duplicate in future, // add the publisher type in the list here. diff --git a/plugins/techdocs/config.d.ts b/plugins/techdocs/config.d.ts index 1dd7319f61..f7903ea3fb 100644 --- a/plugins/techdocs/config.d.ts +++ b/plugins/techdocs/config.d.ts @@ -117,21 +117,23 @@ export interface Config { | { /** * attr: 'type' - accepts a string value - * e.g. type: 'azureStorage' - * alternatives: 'azureStorage' etc. + * e.g. type: 'azureBlobStorage' + * alternatives: 'azureBlobStorage' etc. * @see http://backstage.io/docs/features/techdocs/configuration */ - type: 'azureStorage'; + type: 'azureBlobStorage'; /** - * azureStorage required when 'type' is set to azureStorage + * azureBlobStorage required when 'type' is set to azureBlobStorage */ - azureStorage?: { + azureBlobStorage?: { /** - * Credentials used to access a storage container + * (Optional) Credentials used to access a storage container. + * If not set, environment variables will be used to authenticate. + * https://docs.microsoft.com/en-us/azure/storage/common/storage-auth?toc=/azure/storage/blobs/toc.json * @visibility secret */ - credentials: { + credentials?: { /** * Account access name * attr: 'account' - accepts a string value @@ -146,7 +148,7 @@ export interface Config { accountKey: string; }; /** - * Cloud Storage Container Name + * (Required) Cloud Storage Container Name * attr: 'containerName' - accepts a string value * @visibility backend */ From 74d34c8ff9ab85dc8748c333c0348d113d2cab30 Mon Sep 17 00:00:00 2001 From: vitorgrenzel Date: Wed, 20 Jan 2021 18:00:12 -0300 Subject: [PATCH 24/99] feat(docs): update Azure Blob Storage --- docs/features/techdocs/using-cloud-storage.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index 9ae14abfe3..402ae657ea 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -223,8 +223,10 @@ techdocs: type: 'azureBlobStorage' azureBlobStorage: credentials: - account: 'account' - accountKey: 'accountKey' + account: + $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT + accountKey: + $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_KEY ``` **3. Azure Blob Storage Container** From 2d8d7697d60d3bc685787f80eaa04d6dc6759219 Mon Sep 17 00:00:00 2001 From: vitorgrenzel Date: Wed, 20 Jan 2021 18:20:17 -0300 Subject: [PATCH 25/99] feat(techdocs-common): update Azure Blob Storage --- packages/techdocs-common/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 09893bd38e..1834248c3a 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -36,8 +36,8 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@azure/storage-blob": "^12.3.0", "@aws-sdk/client-s3": "^3.1.0", + "@azure/storage-blob": "^12.4.0", "@backstage/backend-common": "^0.5.1", "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", From 0be9694aa6997d78b091ddcbb3fe3a5763ff283c Mon Sep 17 00:00:00 2001 From: vitorgrenzel Date: Fri, 22 Jan 2021 16:09:06 -0300 Subject: [PATCH 26/99] feat(techdocs-common): update Azure Blob Storage --- docs/features/techdocs/configuration.md | 12 +-- docs/features/techdocs/using-cloud-storage.md | 76 ++++++++++++------- packages/techdocs-common/package.json | 1 + .../stages/publish/azureBlobStorage.test.ts | 2 +- .../src/stages/publish/azureBlobStorage.ts | 48 +++++++----- .../src/stages/publish/publish.test.ts | 2 +- plugins/techdocs/config.d.ts | 14 ++-- 7 files changed, 96 insertions(+), 59 deletions(-) diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index efed37fc69..f588be7c8c 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -91,12 +91,14 @@ techdocs: # (Required) Azure Blob Storage Container Name containerName: 'techdocs-storage' - # (Optional) An API key is required to write to a storage container. - # If not set, environment variables will be used to authenticate. - #https://docs.microsoft.com/en-us/azure/storage/common/storage-auth?toc=/azure/storage/blobs/toc.json + # (Required) An account name is required to write to a storage blob container. + # https://docs.microsoft.com/pt-br/rest/api/storageservices/authorize-with-shared-key credentials: - account: - $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT + accountName: + $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_NAME + # (Optional) An account key is required to write to a storage container. + # If missing,AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET environment variable will be used. + # https://docs.microsoft.com/en-us/azure/storage/common/storage-auth?toc=/azure/storage/blobs/toc.json accountKey: $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_KEY ``` diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index 402ae657ea..f384270379 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -212,33 +212,17 @@ techdocs: type: 'azureBlobStorage' ``` -**2. Service account credentials** +**2. Create an Azure Blob Storage Container** -To get credentials, access the Azure Portal and go to "Settings > Access Keys", -and get your Storage account name and Primary Key. +Create a dedicated container for TechDocs sites. +[Refer to the official documentation](https://docs.microsoft.com/pt-br/azure/storage/blobs/storage-quickstart-blobs-portal). -```yaml -techdocs: - publisher: - type: 'azureBlobStorage' - azureBlobStorage: - credentials: - account: - $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT - accountKey: - $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_KEY -``` +TechDocs will publish documentation to this container and will fetch files from +here to serve documentation in Backstage. Note that the container names are +globally unique. -**3. Azure Blob Storage Container** - -Create a dedicated container for TechDocs sites. techdocs-backend will publish -documentation to this container. TechDocs will fetch files from here to serve -documentation in Backstage. - -To create a new container, access "Blob Service > Containers > New Container". - -Set the name of the container to -`techdocs.publisher.azureBlobStorage.containerName`. +Set the config `techdocs.publisher.azureBlobStorage.containerName` in your +`app-config.yaml` to the name of the container you just created. ```yaml techdocs: @@ -246,9 +230,49 @@ techdocs: type: 'azureBlobStorage' azureBlobStorage: containerName: 'name-of-techdocs-storage-container' +``` + +**3a. (Recommended) Authentication using environment variable** + +Set the config `techdocs.publisher.azureBlobStorage.credentials.accountName` in +your `app-config.yaml` to the your account name. + +The storage blob client will automatically use the environment variable +`AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET` to authenticate with +Azure Blob Storage. +https://docs.microsoft.com/pt-br/azure/storage/common/storage-auth-aad for more +details. + +```yaml +techdocs: + publisher: + type: 'azureBlobStorage' + azureBlobStorage: + containerName: 'name-of-techdocs-storage-bucket' credentials: - account: - $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT + accountName: + $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_NAME +``` + +**3b. Authentication using app-config.yaml** + +If you do not prefer (3a) and optionally like to use a service account, you can +follow these steps. + +To get credentials, access the Azure Portal and go to "Settings > Access Keys", +and get your Storage account name and Primary Key. +https://docs.microsoft.com/pt-br/rest/api/storageservices/authorize-with-shared-key +for more details. + +```yaml +techdocs: + publisher: + type: 'azureBlobStorage' + azureBlobStorage: + containerName: 'name-of-techdocs-storage-bucket' + credentials: + accountName: + $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_NAME accountKey: $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_KEY ``` diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 1834248c3a..2a065600a1 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -37,6 +37,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.1.0", + "@azure/identity": "^1.2.2", "@azure/storage-blob": "^12.4.0", "@backstage/backend-common": "^0.5.1", "@backstage/catalog-model": "^0.7.0", diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts index 95e4a52f62..9d2ee1ba9b 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts @@ -57,7 +57,7 @@ beforeEach(async () => { type: 'azureBlobStorage', azureBlobStorage: { credentials: { - account: 'account', + accountName: 'accountName', accountKey: 'accountKey', }, containerName: 'containerName', diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts index 7ba4d35892..abfa9d71bf 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts @@ -20,6 +20,7 @@ import { BlobUploadCommonResponse, StorageSharedKeyCredential, } from '@azure/storage-blob'; +import { DefaultAzureCredential } from '@azure/identity'; import { Logger } from 'winston'; import { Entity, EntityName } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; @@ -43,29 +44,39 @@ export class AzureBlobStoragePublish implements PublisherBase { ); } catch (error) { throw new Error( - "Since techdocs.publisher.type is set to 'awsS3' in your app config, " + - 'techdocs.publisher.awsS3.bucketName is required.', + "Since techdocs.publisher.type is set to 'azureBlobStorage' in your app config, " + + 'techdocs.publisher.azureBlobStorage.containerName is required.', ); } - // Credentials is an optional config. If missing, default AWS environment variables - // or AWS shared credentials file at ~/.aws/credentials will be used to authenticate - // https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html - // https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-shared.html - let account = ''; - let accountKey = ''; - account = - config.getOptionalString( - 'techdocs.publisher.azureBlobStorage.credentials.account', - ) || ''; - accountKey = - config.getOptionalString( - 'techdocs.publisher.azureBlobStorage.credentials.accountKey', - ) || ''; + let accountName = ''; + try { + accountName = config.getString( + 'techdocs.publisher.azureBlobStorage.credentials.accountName', + ); + } catch (error) { + throw new Error( + "Since techdocs.publisher.type is set to 'azureBlobStorage' in your app config, " + + 'techdocs.publisher.azureBlobStorage.credentials.accountName is required.', + ); + } + + // Credentials is an optional config. If missing, default Azure Blob Storage environment variables + // https://docs.microsoft.com/pt-br/azure/storage/common/storage-auth-aad-app + const accountKey = config.getOptionalString( + 'techdocs.publisher.azureBlobStorage.credentials.accountKey', + ); + + let credential; + if (accountKey) { + console.log('accountKey =>', accountKey); + credential = new StorageSharedKeyCredential(accountName, accountKey); + } else { + credential = new DefaultAzureCredential(); + } - const credential = new StorageSharedKeyCredential(account, accountKey); const storageClient = new BlobServiceClient( - `https://${account}.blob.core.windows.net`, + `https://${accountName}.blob.core.windows.net`, credential, ); @@ -129,7 +140,6 @@ export class AzureBlobStoragePublish implements PublisherBase { `${entityRootDir}/${relativeFilePath}`, ); // Azure Blob Storage Container file relative path - // TODO: Upload in chunks of ~10 files instead of all files at once. return limiter(async () => { await uploadPromises.push( this.storageClient diff --git a/packages/techdocs-common/src/stages/publish/publish.test.ts b/packages/techdocs-common/src/stages/publish/publish.test.ts index a563e53a9b..4ae59a597e 100644 --- a/packages/techdocs-common/src/stages/publish/publish.test.ts +++ b/packages/techdocs-common/src/stages/publish/publish.test.ts @@ -115,7 +115,7 @@ describe('Publisher', () => { type: 'azureBlobStorage', azureBlobStorage: { credentials: { - account: 'account', + accountName: 'accountName', accountKey: 'accountKey', }, containerName: 'containerName', diff --git a/plugins/techdocs/config.d.ts b/plugins/techdocs/config.d.ts index f7903ea3fb..0af3ab4290 100644 --- a/plugins/techdocs/config.d.ts +++ b/plugins/techdocs/config.d.ts @@ -128,24 +128,24 @@ export interface Config { */ azureBlobStorage?: { /** - * (Optional) Credentials used to access a storage container. - * If not set, environment variables will be used to authenticate. - * https://docs.microsoft.com/en-us/azure/storage/common/storage-auth?toc=/azure/storage/blobs/toc.json + * (Required) Credentials used to access a storage container. * @visibility secret */ - credentials?: { + credentials: { /** * Account access name * attr: 'account' - accepts a string value * @visibility secret */ - account: string; + accountName: string; /** - * Account secret primary key + * (Optional) Account secret primary key + * If not set, environment variables will be used to authenticate. + * https://docs.microsoft.com/en-us/azure/storage/common/storage-auth?toc=/azure/storage/blobs/toc.json * attr: 'accountKey' - accepts a string value * @visibility secret */ - accountKey: string; + accountKey?: string; }; /** * (Required) Cloud Storage Container Name From 0024d6d8c0c44ce074cc047729c77fea3448fa47 Mon Sep 17 00:00:00 2001 From: vitorgrenzel Date: Mon, 25 Jan 2021 10:01:30 -0300 Subject: [PATCH 27/99] feat(techdocs-common): update Azure Blob Storage --- docs/features/techdocs/using-cloud-storage.md | 6 ++-- .../src/stages/publish/azureBlobStorage.ts | 9 +++--- .../src/stages/publish/publish.test.ts | 31 +++++++++++++++++++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index f384270379..393e478302 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -215,7 +215,7 @@ techdocs: **2. Create an Azure Blob Storage Container** Create a dedicated container for TechDocs sites. -[Refer to the official documentation](https://docs.microsoft.com/pt-br/azure/storage/blobs/storage-quickstart-blobs-portal). +[Refer to the official documentation](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-portal). TechDocs will publish documentation to this container and will fetch files from here to serve documentation in Backstage. Note that the container names are @@ -240,7 +240,9 @@ your `app-config.yaml` to the your account name. The storage blob client will automatically use the environment variable `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET` to authenticate with Azure Blob Storage. -https://docs.microsoft.com/pt-br/azure/storage/common/storage-auth-aad for more +[Steps to create the service where the variables can be retrieved from](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal). + +https://docs.microsoft.com/en-us/azure/storage/common/storage-auth-aad for more details. ```yaml diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts index abfa9d71bf..1b4e59a714 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts @@ -61,15 +61,14 @@ export class AzureBlobStoragePublish implements PublisherBase { ); } - // Credentials is an optional config. If missing, default Azure Blob Storage environment variables - // https://docs.microsoft.com/pt-br/azure/storage/common/storage-auth-aad-app + // Credentials is an optional config. If missing, default Azure Blob Storage environment variables will be used. + // https://docs.microsoft.com/en-us/azure/storage/common/storage-auth-aad-app const accountKey = config.getOptionalString( 'techdocs.publisher.azureBlobStorage.credentials.accountKey', ); let credential; if (accountKey) { - console.log('accountKey =>', accountKey); credential = new StorageSharedKeyCredential(accountName, accountKey); } else { credential = new DefaultAzureCredential(); @@ -91,8 +90,8 @@ export class AzureBlobStoragePublish implements PublisherBase { .catch(reason => { logger.error( `Could not retrieve metadata about the Azure Blob Storage container ${containerName}. ` + - 'Make sure the Azure project and the container exists and the access key located at the path ' + - "techdocs.publisher.azureBlobStorage.credentials defined in app config has the role 'Storage Object Creator'. " + + 'Make sure that the Azure project and container exist and the access key is setup correctly ' + + 'techdocs.publisher.azureBlobStorage.credentials defined in app config has correct permissions. ' + 'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage', ); throw new Error( diff --git a/packages/techdocs-common/src/stages/publish/publish.test.ts b/packages/techdocs-common/src/stages/publish/publish.test.ts index 4ae59a597e..d1faf82b78 100644 --- a/packages/techdocs-common/src/stages/publish/publish.test.ts +++ b/packages/techdocs-common/src/stages/publish/publish.test.ts @@ -31,6 +31,10 @@ const discovery: jest.Mocked = { }; describe('Publisher', () => { + beforeEach(() => { + jest.resetModules(); // clear the cache + }); + it('should create local publisher by default', async () => { const mockConfig = new ConfigReader({ techdocs: { @@ -130,4 +134,31 @@ describe('Publisher', () => { }); expect(publisher).toBeInstanceOf(AzureBlobStoragePublish); }); + + it('should create Azure Blob Storage publisher from environment variables', async () => { + process.env.AZURE_TENANT_ID = 'AZURE_TENANT_ID'; + process.env.AZURE_CLIENT_ID = 'AZURE_CLIENT_ID'; + process.env.AZURE_CLIENT_SECRET = 'AZURE_CLIENT_SECRET'; + + const mockConfig = new ConfigReader({ + techdocs: { + requestUrl: 'http://localhost:7000', + publisher: { + type: 'azureBlobStorage', + azureBlobStorage: { + credentials: { + accountName: 'accountName', + }, + containerName: 'containerName', + }, + }, + }, + }); + + const publisher = await Publisher.fromConfig(mockConfig, { + logger, + discovery, + }); + expect(publisher).toBeInstanceOf(AzureBlobStoragePublish); + }); }); From 9197ac3a0baf3281d016e95c7fdb1c0f2ce8b32f Mon Sep 17 00:00:00 2001 From: vitorgrenzel Date: Mon, 25 Jan 2021 10:20:56 -0300 Subject: [PATCH 28/99] feat(techdocs-common): update tests Azure Blob Storage --- packages/techdocs-common/__mocks__/@azure/identity.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/techdocs-common/__mocks__/@azure/identity.ts diff --git a/packages/techdocs-common/__mocks__/@azure/identity.ts b/packages/techdocs-common/__mocks__/@azure/identity.ts new file mode 100644 index 0000000000..0fb8a75ebd --- /dev/null +++ b/packages/techdocs-common/__mocks__/@azure/identity.ts @@ -0,0 +1 @@ +export class DefaultAzureCredential {} From a2feeb288d08ff7e1f8daf9e323d584e7ee57e9b Mon Sep 17 00:00:00 2001 From: vitorgrenzel Date: Mon, 25 Jan 2021 11:24:12 -0300 Subject: [PATCH 29/99] feat(techdocs-common): update tests Azure Blob Storage --- .../__mocks__/@azure/identity.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/techdocs-common/__mocks__/@azure/identity.ts b/packages/techdocs-common/__mocks__/@azure/identity.ts index 0fb8a75ebd..cc89a4d514 100644 --- a/packages/techdocs-common/__mocks__/@azure/identity.ts +++ b/packages/techdocs-common/__mocks__/@azure/identity.ts @@ -1 +1,20 @@ -export class DefaultAzureCredential {} +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export class DefaultAzureCredential { + /** + * Creates an instance of the DefaultAzureCredential class. + */ +} From ee271bf86ed398d9cb27f26a588adbf5a8f87732 Mon Sep 17 00:00:00 2001 From: vitorgrenzel Date: Mon, 25 Jan 2021 11:46:25 -0300 Subject: [PATCH 30/99] feat(docs): update Azure Blob Storage --- docs/features/techdocs/configuration.md | 2 +- docs/features/techdocs/using-cloud-storage.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index f588be7c8c..bb44050dcf 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -92,7 +92,7 @@ techdocs: containerName: 'techdocs-storage' # (Required) An account name is required to write to a storage blob container. - # https://docs.microsoft.com/pt-br/rest/api/storageservices/authorize-with-shared-key + # https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key credentials: accountName: $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_NAME diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index 393e478302..de2d505c87 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -263,7 +263,7 @@ follow these steps. To get credentials, access the Azure Portal and go to "Settings > Access Keys", and get your Storage account name and Primary Key. -https://docs.microsoft.com/pt-br/rest/api/storageservices/authorize-with-shared-key +https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key for more details. ```yaml From a08c4b0b057bdd452ae1dac9fb6df55835d3f697 Mon Sep 17 00:00:00 2001 From: Kevin Lee Date: Thu, 28 Jan 2021 14:35:08 -0800 Subject: [PATCH 31/99] Add check for outdated/duplicate packages to yarn start --- .changeset/orange-pets-whisper.md | 5 +++++ packages/cli/src/commands/app/serve.ts | 28 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 .changeset/orange-pets-whisper.md diff --git a/.changeset/orange-pets-whisper.md b/.changeset/orange-pets-whisper.md new file mode 100644 index 0000000000..e96c210e43 --- /dev/null +++ b/.changeset/orange-pets-whisper.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add check for outdated/duplicate packages to yarn start diff --git a/packages/cli/src/commands/app/serve.ts b/packages/cli/src/commands/app/serve.ts index dc165ecb90..e0f2881332 100644 --- a/packages/cli/src/commands/app/serve.ts +++ b/packages/cli/src/commands/app/serve.ts @@ -15,12 +15,40 @@ */ import fs from 'fs-extra'; +import chalk from 'chalk'; import { Command } from 'commander'; import { serveBundle } from '../../lib/bundler'; import { loadCliConfig } from '../../lib/config'; import { paths } from '../../lib/paths'; +import { Lockfile } from '../../lib/versioning'; +import { includedFilter } from '../versions/lint'; export default async (cmd: Command) => { + const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); + const result = lockfile.analyze({ + filter: includedFilter, + }); + const problemPackages = result.newVersions.map(({ name }) => name); + + if (problemPackages.length > 1) { + console.log( + chalk.yellow( + `The following packages may be outdated or have duplicate installations: + + ${problemPackages.join(', ')} + `, + ), + ); + console.log( + chalk.yellow( + `This can be resolved using the following command: + + yarn backstage-cli versions:check --fix + `, + ), + ); + } + const { name } = await fs.readJson(paths.resolveTarget('package.json')); const waitForExit = await serveBundle({ entry: 'src/index', From 8de07b3e658bfd1900db0d6362f2d2020a1f4253 Mon Sep 17 00:00:00 2001 From: Kevin Lee Date: Thu, 28 Jan 2021 19:55:17 -0800 Subject: [PATCH 32/99] Prepend caution icon to yarn start warning and check for package newRanges --- packages/cli/src/commands/app/serve.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/app/serve.ts b/packages/cli/src/commands/app/serve.ts index e0f2881332..b542030109 100644 --- a/packages/cli/src/commands/app/serve.ts +++ b/packages/cli/src/commands/app/serve.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import chalk from 'chalk'; +import uniq from 'lodash/uniq'; import { Command } from 'commander'; import { serveBundle } from '../../lib/bundler'; import { loadCliConfig } from '../../lib/config'; @@ -28,20 +29,22 @@ export default async (cmd: Command) => { const result = lockfile.analyze({ filter: includedFilter, }); - const problemPackages = result.newVersions.map(({ name }) => name); + const problemPackages = [...result.newVersions, ...result.newRanges].map( + ({ name }) => name, + ); if (problemPackages.length > 1) { console.log( chalk.yellow( - `The following packages may be outdated or have duplicate installations: + `⚠️ Some of the following packages may be outdated or have duplicate installations: - ${problemPackages.join(', ')} + ${uniq(problemPackages).join(', ')} `, ), ); console.log( chalk.yellow( - `This can be resolved using the following command: + `⚠️ This can be resolved using the following command: yarn backstage-cli versions:check --fix `, From 019fe39a0873ed053b86c44a8778b65f4f824d80 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 26 Jan 2021 18:53:29 +0100 Subject: [PATCH 33/99] Createa @backstage/plugin-catalog-common-react package --- .changeset/wet-suits-live.md | 19 ++++++ .changeset/wild-cows-exercise.md | 8 +++ packages/app/package.json | 1 + .../app/src/components/catalog/EntityPage.tsx | 21 +++---- packages/create-app/src/lib/versions.ts | 2 + .../default-app/packages/app/package.json.hbs | 1 + .../app/src/components/catalog/EntityPage.tsx | 4 +- plugins/api-docs/package.json | 2 +- .../ApiExplorerPage/ApiExplorerPage.test.tsx | 5 +- .../ApiExplorerPage/ApiExplorerPage.tsx | 2 +- .../ApisCards/ConsumedApisCard.test.tsx | 5 +- .../ApisCards/ProvidedApisCard.test.tsx | 5 +- .../ConsumingComponentsCard.test.tsx | 5 +- .../ProvidingComponentsCard.test.tsx | 5 +- .../src/components/EntityLink/EntityLink.tsx | 5 +- .../src/components/useRelatedEntities.ts | 2 +- plugins/catalog-common-react/.eslintrc.js | 3 + plugins/catalog-common-react/README.md | 17 ++++++ plugins/catalog-common-react/package.json | 60 +++++++++++++++++++ plugins/catalog-common-react/src/api.ts | 24 ++++++++ .../EntityRefLink/EntityRefLink.test.tsx | 0 .../EntityRefLink/EntityRefLink.tsx | 3 - .../EntityRefLink/EntityRefLinks.test.tsx | 0 .../EntityRefLink/EntityRefLinks.tsx | 1 - .../components/EntityRefLink/format.test.ts | 0 .../src/components/EntityRefLink/format.ts | 0 .../src/components/EntityRefLink/index.ts | 0 .../src/components/index.ts | 16 +++++ .../catalog-common-react/src/hooks/index.ts | 17 ++++++ .../src/hooks/useEntity.ts | 4 +- .../src/hooks}/useEntityCompoundName.ts | 0 plugins/catalog-common-react/src/index.ts | 26 ++++++++ .../src/routes.ts | 0 .../catalog-common-react/src/setupTests.ts | 18 ++++++ plugins/catalog-import/package.json | 2 +- .../src/components/ComponentConfigDisplay.tsx | 35 ++++++----- .../components/ImportComponentForm.test.tsx | 13 ++-- .../src/components/ImportComponentForm.tsx | 8 +-- .../components/ImportComponentPage.test.tsx | 15 ++--- plugins/catalog/package.json | 3 +- .../src/components/AboutCard/AboutContent.tsx | 2 +- .../components/CatalogPage/CatalogPage.tsx | 6 +- .../components/CatalogTable/CatalogTable.tsx | 10 ++-- .../EntityLayout/EntityLayout.test.tsx | 16 ++--- .../components/EntityLayout/EntityLayout.tsx | 24 ++++---- .../EntityLoaderProvider.tsx | 5 +- .../EntityPageLayout/EntityPageLayout.tsx | 7 ++- .../EntityProvider/EntityProvider.tsx | 2 +- .../EntitySwitch/EntitySwitch.test.tsx | 8 +-- .../components/EntitySwitch/EntitySwitch.tsx | 10 ++-- plugins/catalog/src/components/Router.tsx | 9 ++- .../UnregisterEntityDialog.tsx | 6 +- plugins/catalog/src/components/useOwnUser.ts | 4 +- plugins/catalog/src/extensions.tsx | 5 +- .../src/filter/EntityFilterGroupsProvider.tsx | 2 +- plugins/catalog/src/index.ts | 8 +-- plugins/catalog/src/plugin.ts | 15 ++--- plugins/circleci/package.json | 2 +- plugins/circleci/src/state/useBuilds.ts | 4 +- plugins/cloudbuild/package.json | 1 - plugins/github-actions/package.json | 1 - plugins/jenkins/package.json | 2 +- .../components/useProjectSlugFromEntity.ts | 2 +- plugins/kafka/package.json | 2 +- .../useConsumerGroupsForEntity.test.tsx | 8 +-- .../useConsumerGroupsForEntity.ts | 2 +- ...useConsumerGroupsOffsetsForEntity.test.tsx | 12 ++-- plugins/lighthouse/package.json | 2 +- .../AuditList/AuditListForEntity.test.tsx | 19 +++--- .../Cards/LastLighthouseAuditCard.test.tsx | 14 ++--- .../src/hooks/useWebsiteForEntity.test.tsx | 12 ++-- .../src/hooks/useWebsiteForEntity.ts | 6 +- plugins/org/package.json | 2 +- .../Group/GroupProfile/GroupProfileCard.tsx | 2 +- .../MembersList/MembersListCard.test.tsx | 9 ++- .../Group/MembersList/MembersListCard.tsx | 5 +- .../Cards/OwnershipCard/OwnershipCard.tsx | 2 +- .../User/UserProfileCard/UserProfileCard.tsx | 2 +- plugins/register-component/package.json | 2 +- .../RegisterComponentPage.test.tsx | 2 +- .../RegisterComponentPage.tsx | 20 +++---- .../RegisterComponentResultDialog.tsx | 5 +- plugins/rollbar/package.json | 2 +- .../RollbarHome/RollbarHome.test.tsx | 5 +- .../RollbarProjectPage.test.tsx | 7 ++- plugins/rollbar/src/hooks/useCatalogEntity.ts | 4 +- .../rollbar/src/hooks/useRollbarEntities.ts | 4 +- plugins/scaffolder/package.json | 2 +- .../ScaffolderPage/ScaffolderPage.tsx | 2 +- .../TemplatePage/TemplatePage.test.tsx | 5 +- .../components/TemplatePage/TemplatePage.tsx | 8 +-- plugins/search/package.json | 2 +- plugins/search/src/apis.ts | 2 +- .../components/SearchResult/SearchResult.tsx | 16 +++-- plugins/sentry/dev/index.tsx | 6 +- plugins/sentry/package.json | 2 +- plugins/sentry/src/extensions.tsx | 4 +- plugins/techdocs/package.json | 2 +- .../reader/components/TechDocsHome.test.tsx | 5 +- .../src/reader/components/TechDocsHome.tsx | 2 +- 100 files changed, 488 insertions(+), 228 deletions(-) create mode 100644 .changeset/wet-suits-live.md create mode 100644 .changeset/wild-cows-exercise.md create mode 100644 plugins/catalog-common-react/.eslintrc.js create mode 100644 plugins/catalog-common-react/README.md create mode 100644 plugins/catalog-common-react/package.json create mode 100644 plugins/catalog-common-react/src/api.ts rename plugins/{catalog => catalog-common-react}/src/components/EntityRefLink/EntityRefLink.test.tsx (100%) rename plugins/{catalog => catalog-common-react}/src/components/EntityRefLink/EntityRefLink.tsx (91%) rename plugins/{catalog => catalog-common-react}/src/components/EntityRefLink/EntityRefLinks.test.tsx (100%) rename plugins/{catalog => catalog-common-react}/src/components/EntityRefLink/EntityRefLinks.tsx (96%) rename plugins/{catalog => catalog-common-react}/src/components/EntityRefLink/format.test.ts (100%) rename plugins/{catalog => catalog-common-react}/src/components/EntityRefLink/format.ts (100%) rename plugins/{catalog => catalog-common-react}/src/components/EntityRefLink/index.ts (100%) create mode 100644 plugins/catalog-common-react/src/components/index.ts create mode 100644 plugins/catalog-common-react/src/hooks/index.ts rename plugins/{catalog => catalog-common-react}/src/hooks/useEntity.ts (94%) rename plugins/{catalog/src/components => catalog-common-react/src/hooks}/useEntityCompoundName.ts (100%) create mode 100644 plugins/catalog-common-react/src/index.ts rename plugins/{catalog => catalog-common-react}/src/routes.ts (100%) create mode 100644 plugins/catalog-common-react/src/setupTests.ts diff --git a/.changeset/wet-suits-live.md b/.changeset/wet-suits-live.md new file mode 100644 index 0000000000..c10a226367 --- /dev/null +++ b/.changeset/wet-suits-live.md @@ -0,0 +1,19 @@ +--- +'@backstage/plugin-api-docs': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-circleci': patch +'@backstage/plugin-cloudbuild': patch +'@backstage/plugin-github-actions': patch +'@backstage/plugin-jenkins': patch +'@backstage/plugin-kafka': patch +'@backstage/plugin-lighthouse': patch +'@backstage/plugin-org': patch +'@backstage/plugin-register-component': patch +'@backstage/plugin-rollbar': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-search': patch +'@backstage/plugin-sentry': patch +'@backstage/plugin-techdocs': patch +--- + +Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-common-react`. diff --git a/.changeset/wild-cows-exercise.md b/.changeset/wild-cows-exercise.md new file mode 100644 index 0000000000..411d23daee --- /dev/null +++ b/.changeset/wild-cows-exercise.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-catalog': minor +'@backstage/create-app': minor +--- + +`@backstage/plugin-catalog` stopped exporting hooks and helpers for other +plugins. They are migrated to `@backstage/plugin-catalog-common-react`. +Change both your dependencies and imports to the new package. diff --git a/packages/app/package.json b/packages/app/package.json index 36fe7651e6..72ca698f84 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -9,6 +9,7 @@ "@backstage/core": "^0.5.0", "@backstage/plugin-api-docs": "^0.4.3", "@backstage/plugin-catalog": "^0.2.14", + "@backstage/plugin-catalog-common-react": "^0.0.1", "@backstage/plugin-catalog-import": "^0.3.6", "@backstage/plugin-circleci": "^0.2.6", "@backstage/plugin-cloudbuild": "^0.2.7", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index a1ab181211..756d36391f 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -27,11 +27,8 @@ import { ProvidedApisCard, ProvidingComponentsCard, } from '@backstage/plugin-api-docs'; -import { - AboutCard, - EntityPageLayout, - useEntity, -} from '@backstage/plugin-catalog'; +import { AboutCard, EntityPageLayout } from '@backstage/plugin-catalog'; +import { useEntity } from '@backstage/plugin-catalog-common-react'; import { isPluginApplicableToEntity as isCircleCIAvailable, Router as CircleCIRouter, @@ -50,6 +47,7 @@ import { LatestRunCard as JenkinsLatestRunCard, Router as JenkinsRouter, } from '@backstage/plugin-jenkins'; +import { Router as KafkaRouter } from '@backstage/plugin-kafka'; import { Router as KubernetesRouter } from '@backstage/plugin-kubernetes'; import { EmbeddedRouter as LighthouseRouter, @@ -57,13 +55,16 @@ import { LastLighthouseAuditCard, } from '@backstage/plugin-lighthouse'; import { - OwnershipCard, - MembersListCard, GroupProfileCard, + MembersListCard, + OwnershipCard, UserProfileCard, } from '@backstage/plugin-org'; +import { + isPluginApplicableToEntity as isPagerDutyAvailable, + PagerDutyCard, +} from '@backstage/plugin-pagerduty'; import { Router as SentryRouter } from '@backstage/plugin-sentry'; -import { Router as KafkaRouter } from '@backstage/plugin-kafka'; import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs'; import { Button, Grid } from '@material-ui/core'; import { @@ -82,10 +83,6 @@ import { PullRequestsStatsCard, Router as PullRequestsRouter, } from '@roadiehq/backstage-plugin-github-pull-requests'; -import { - isPluginApplicableToEntity as isPagerDutyAvailable, - PagerDutyCard, -} from '@backstage/plugin-pagerduty'; import { isPluginApplicableToEntity as isTravisCIAvailable, RecentTravisCIBuildsWidget, diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index a703fb9305..4723a4b78b 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -42,6 +42,7 @@ import { version as pluginApiDocs } from '../../../../plugins/api-docs/package.j import { version as pluginAppBackend } from '../../../../plugins/app-backend/package.json'; import { version as pluginAuthBackend } from '../../../../plugins/auth-backend/package.json'; import { version as pluginCatalog } from '../../../../plugins/catalog/package.json'; +import { version as pluginCatalogCommonReact } from '../../../../plugins/catalog-common-react/package.json'; import { version as pluginCatalogBackend } from '../../../../plugins/catalog-backend/package.json'; import { version as pluginCatalogImport } from '../../../../plugins/catalog-import/package.json'; import { version as pluginCircleci } from '../../../../plugins/circleci/package.json'; @@ -68,6 +69,7 @@ export const packageVersions = { '@backstage/plugin-app-backend': pluginAppBackend, '@backstage/plugin-auth-backend': pluginAuthBackend, '@backstage/plugin-catalog': pluginCatalog, + '@backstage/plugin-catalog-common-react': pluginCatalogCommonReact, '@backstage/plugin-catalog-backend': pluginCatalogBackend, '@backstage/plugin-catalog-import': pluginCatalogImport, '@backstage/plugin-circleci': pluginCircleci, diff --git a/packages/create-app/templates/default-app/packages/app/package.json.hbs b/packages/create-app/templates/default-app/packages/app/package.json.hbs index e40ed6f03d..99e613bc40 100644 --- a/packages/create-app/templates/default-app/packages/app/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/app/package.json.hbs @@ -10,6 +10,7 @@ "@backstage/core": "^{{version '@backstage/core'}}", "@backstage/plugin-api-docs": "^{{version '@backstage/plugin-api-docs'}}", "@backstage/plugin-catalog": "^{{version '@backstage/plugin-catalog'}}", + "@backstage/plugin-catalog-common-react": "^{{version '@backstage/plugin-catalog-common-react'}}", "@backstage/plugin-catalog-import": "^{{version '@backstage/plugin-catalog-import'}}", "@backstage/plugin-scaffolder": "^{{version '@backstage/plugin-scaffolder'}}", "@backstage/plugin-techdocs": "^{{version '@backstage/plugin-techdocs'}}", diff --git a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx index b5e384f7a9..409c7919db 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx @@ -24,8 +24,10 @@ import { } from '@backstage/plugin-api-docs'; import { AboutCard, EntityPageLayout, - useEntity } from '@backstage/plugin-catalog'; +import { + useEntity +} from '@backstage/plugin-catalog-common-react'; import { isPluginApplicableToEntity as isCircleCIAvailable, Router as CircleCIRouter } from '@backstage/plugin-circleci'; diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 5e3c42bfdf..bdc292f71e 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -32,7 +32,7 @@ "@asyncapi/react-component": "^0.18.2", "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.14", + "@backstage/plugin-catalog-common-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-icons/font": "^1.0.2", "@material-ui/core": "^4.11.0", diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx index 10495bb957..e5e72af9cc 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx @@ -16,7 +16,10 @@ import { Entity } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry, storageApiRef } from '@backstage/core'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; +import { + CatalogApi, + catalogApiRef, +} from '@backstage/plugin-catalog-common-react'; import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import React from 'react'; diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx index a602877525..3779f6a9de 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx @@ -15,7 +15,7 @@ */ import { Content, ContentHeader, SupportButton, useApi } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog'; +import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; import { Button } from '@material-ui/core'; import React from 'react'; import { Link as RouterLink } from 'react-router-dom'; diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx index a4a1e15511..870528b91d 100644 --- a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx @@ -16,7 +16,10 @@ import { Entity, RELATION_CONSUMES_API } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry } from '@backstage/core'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; +import { + CatalogApi, + catalogApiRef, +} from '@backstage/plugin-catalog-common-react'; import { renderInTestApp } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx index 1f42ff9060..a149fcde88 100644 --- a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx @@ -16,7 +16,10 @@ import { Entity, RELATION_PROVIDES_API } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry } from '@backstage/core'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; +import { + CatalogApi, + catalogApiRef, +} from '@backstage/plugin-catalog-common-react'; import { renderInTestApp } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx index 606ff7e77b..428f4d91fe 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx @@ -16,7 +16,10 @@ import { Entity, RELATION_API_CONSUMED_BY } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry } from '@backstage/core'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; +import { + CatalogApi, + catalogApiRef, +} from '@backstage/plugin-catalog-common-react'; import { renderInTestApp } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx index d1cec1722a..3f4b7a545b 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx @@ -16,7 +16,10 @@ import { Entity, RELATION_API_PROVIDED_BY } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry } from '@backstage/core'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; +import { + CatalogApi, + catalogApiRef, +} from '@backstage/plugin-catalog-common-react'; import { renderInTestApp } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; diff --git a/plugins/api-docs/src/components/EntityLink/EntityLink.tsx b/plugins/api-docs/src/components/EntityLink/EntityLink.tsx index 98b2103882..db0437ed83 100644 --- a/plugins/api-docs/src/components/EntityLink/EntityLink.tsx +++ b/plugins/api-docs/src/components/EntityLink/EntityLink.tsx @@ -15,7 +15,10 @@ */ import { Entity } from '@backstage/catalog-model'; -import { entityRoute, entityRouteParams } from '@backstage/plugin-catalog'; +import { + entityRoute, + entityRouteParams, +} from '@backstage/plugin-catalog-common-react'; import { Link } from '@material-ui/core'; import React, { PropsWithChildren } from 'react'; import { generatePath, Link as RouterLink } from 'react-router-dom'; diff --git a/plugins/api-docs/src/components/useRelatedEntities.ts b/plugins/api-docs/src/components/useRelatedEntities.ts index 847ec30578..4c161e8025 100644 --- a/plugins/api-docs/src/components/useRelatedEntities.ts +++ b/plugins/api-docs/src/components/useRelatedEntities.ts @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; import { useApi } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog'; +import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; import { useAsyncRetry } from 'react-use'; // TODO: Maybe this hook is interesting for others too? diff --git a/plugins/catalog-common-react/.eslintrc.js b/plugins/catalog-common-react/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/catalog-common-react/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/catalog-common-react/README.md b/plugins/catalog-common-react/README.md new file mode 100644 index 0000000000..574e23cf62 --- /dev/null +++ b/plugins/catalog-common-react/README.md @@ -0,0 +1,17 @@ +# Catalog Client + +Contains a frontend and backend compatible client for communicating with the +Backstage Catalog. + +Backend code may import and use this package directly. + +However, frontend code will not want to import this package directly - use the +`@backstage/plugin-catalog` package instead, which re-exports all of the types +and classes from this package. Thereby, you will also gain access to its +`catalogApiRef`. + +## Links + +- [Default frontend part of the catalog](https://github.com/spotify/backstage/tree/master/plugins/catalog) +- [Default backend part of the catalog](https://github.com/spotify/backstage/tree/master/plugins/catalog-backend) +- [The Backstage homepage](https://backstage.io) diff --git a/plugins/catalog-common-react/package.json b/plugins/catalog-common-react/package.json new file mode 100644 index 0000000000..11b8d3b1f4 --- /dev/null +++ b/plugins/catalog-common-react/package.json @@ -0,0 +1,60 @@ +{ + "name": "@backstage/plugin-catalog-common-react", + "version": "0.0.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/plugin-catalog-common-react" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-client": "^0.3.5", + "@backstage/catalog-model": "^0.7.0", + "@backstage/core": "^0.5.0", + "@material-ui/core": "^4.11.0", + "@types/react": "^16.9", + "react": "^16.13.1", + "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0", + "react-use": "^15.3.3" + }, + "devDependencies": { + "@backstage/cli": "^0.4.7", + "@backstage/dev-utils": "^0.1.8", + "@backstage/test-utils": "^0.1.6", + "@microsoft/microsoft-graph-types": "^1.25.0", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/react-hooks": "^3.3.0", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^12.0.0", + "cross-fetch": "^3.0.6", + "msw": "^0.21.2", + "react-test-renderer": "^16.13.1" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/catalog-common-react/src/api.ts b/plugins/catalog-common-react/src/api.ts new file mode 100644 index 0000000000..9379fa7fb3 --- /dev/null +++ b/plugins/catalog-common-react/src/api.ts @@ -0,0 +1,24 @@ +/* + * 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 { CatalogApi } from '@backstage/catalog-client'; +import { createApiRef } from '@backstage/core'; + +export const catalogApiRef = createApiRef({ + id: 'plugin.catalog.service', + description: + 'Used by the Catalog plugin to make requests to accompanying backend', +}); diff --git a/plugins/catalog/src/components/EntityRefLink/EntityRefLink.test.tsx b/plugins/catalog-common-react/src/components/EntityRefLink/EntityRefLink.test.tsx similarity index 100% rename from plugins/catalog/src/components/EntityRefLink/EntityRefLink.test.tsx rename to plugins/catalog-common-react/src/components/EntityRefLink/EntityRefLink.test.tsx diff --git a/plugins/catalog/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-common-react/src/components/EntityRefLink/EntityRefLink.tsx similarity index 91% rename from plugins/catalog/src/components/EntityRefLink/EntityRefLink.tsx rename to plugins/catalog-common-react/src/components/EntityRefLink/EntityRefLink.tsx index 64b2bb8615..ae6f807275 100644 --- a/plugins/catalog/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-common-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -31,9 +31,6 @@ type EntityRefLinkProps = { children?: React.ReactNode; }; -// TODO: This component is private for now, as it should probably belong into -// some kind of helper module for the catalog plugin to avoid a dependency on -// the catalog plugin itself. export const EntityRefLink = ({ entityRef, defaultKind, diff --git a/plugins/catalog/src/components/EntityRefLink/EntityRefLinks.test.tsx b/plugins/catalog-common-react/src/components/EntityRefLink/EntityRefLinks.test.tsx similarity index 100% rename from plugins/catalog/src/components/EntityRefLink/EntityRefLinks.test.tsx rename to plugins/catalog-common-react/src/components/EntityRefLink/EntityRefLinks.test.tsx diff --git a/plugins/catalog/src/components/EntityRefLink/EntityRefLinks.tsx b/plugins/catalog-common-react/src/components/EntityRefLink/EntityRefLinks.tsx similarity index 96% rename from plugins/catalog/src/components/EntityRefLink/EntityRefLinks.tsx rename to plugins/catalog-common-react/src/components/EntityRefLink/EntityRefLinks.tsx index 3c8beaec6f..fb99fad176 100644 --- a/plugins/catalog/src/components/EntityRefLink/EntityRefLinks.tsx +++ b/plugins/catalog-common-react/src/components/EntityRefLink/EntityRefLinks.tsx @@ -22,7 +22,6 @@ type EntityRefLinksProps = { defaultKind?: string; }; -// TODO: Move into a shared helper package export const EntityRefLinks = ({ entityRefs, defaultKind, diff --git a/plugins/catalog/src/components/EntityRefLink/format.test.ts b/plugins/catalog-common-react/src/components/EntityRefLink/format.test.ts similarity index 100% rename from plugins/catalog/src/components/EntityRefLink/format.test.ts rename to plugins/catalog-common-react/src/components/EntityRefLink/format.test.ts diff --git a/plugins/catalog/src/components/EntityRefLink/format.ts b/plugins/catalog-common-react/src/components/EntityRefLink/format.ts similarity index 100% rename from plugins/catalog/src/components/EntityRefLink/format.ts rename to plugins/catalog-common-react/src/components/EntityRefLink/format.ts diff --git a/plugins/catalog/src/components/EntityRefLink/index.ts b/plugins/catalog-common-react/src/components/EntityRefLink/index.ts similarity index 100% rename from plugins/catalog/src/components/EntityRefLink/index.ts rename to plugins/catalog-common-react/src/components/EntityRefLink/index.ts diff --git a/plugins/catalog-common-react/src/components/index.ts b/plugins/catalog-common-react/src/components/index.ts new file mode 100644 index 0000000000..7719984d2b --- /dev/null +++ b/plugins/catalog-common-react/src/components/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './EntityRefLink'; diff --git a/plugins/catalog-common-react/src/hooks/index.ts b/plugins/catalog-common-react/src/hooks/index.ts new file mode 100644 index 0000000000..77de70b8d4 --- /dev/null +++ b/plugins/catalog-common-react/src/hooks/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { EntityContext, useEntity, useEntityFromUrl } from './useEntity'; +export { useEntityCompoundName } from './useEntityCompoundName'; diff --git a/plugins/catalog/src/hooks/useEntity.ts b/plugins/catalog-common-react/src/hooks/useEntity.ts similarity index 94% rename from plugins/catalog/src/hooks/useEntity.ts rename to plugins/catalog-common-react/src/hooks/useEntity.ts index 289c4e26aa..faeeca11ed 100644 --- a/plugins/catalog/src/hooks/useEntity.ts +++ b/plugins/catalog-common-react/src/hooks/useEntity.ts @@ -18,8 +18,8 @@ import { errorApiRef, useApi } from '@backstage/core'; import { createContext, useContext, useEffect } from 'react'; import { useNavigate } from 'react-router'; import { useAsync } from 'react-use'; -import { useEntityCompoundName } from '../components/useEntityCompoundName'; -import { catalogApiRef } from '../plugin'; +import { useEntityCompoundName } from './useEntityCompoundName'; +import { catalogApiRef } from '../api'; type EntityLoadingStatus = { entity?: Entity; diff --git a/plugins/catalog/src/components/useEntityCompoundName.ts b/plugins/catalog-common-react/src/hooks/useEntityCompoundName.ts similarity index 100% rename from plugins/catalog/src/components/useEntityCompoundName.ts rename to plugins/catalog-common-react/src/hooks/useEntityCompoundName.ts diff --git a/plugins/catalog-common-react/src/index.ts b/plugins/catalog-common-react/src/index.ts new file mode 100644 index 0000000000..b0a9743ea4 --- /dev/null +++ b/plugins/catalog-common-react/src/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from '@backstage/catalog-client'; +export { catalogApiRef } from './api'; +export * from './components'; +export * from './hooks'; +export { + catalogRouteRef, + entityRoute, + entityRouteParams, + entityRouteRef, + rootRoute, +} from './routes'; diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog-common-react/src/routes.ts similarity index 100% rename from plugins/catalog/src/routes.ts rename to plugins/catalog-common-react/src/routes.ts diff --git a/plugins/catalog-common-react/src/setupTests.ts b/plugins/catalog-common-react/src/setupTests.ts new file mode 100644 index 0000000000..aea2220869 --- /dev/null +++ b/plugins/catalog-common-react/src/setupTests.ts @@ -0,0 +1,18 @@ +/* + * 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 '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 5a4dbf8798..6deb066705 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -33,7 +33,7 @@ "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", "@backstage/integration": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.14", + "@backstage/plugin-catalog-common-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx b/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx index 96b1bf2298..ff8d28e654 100644 --- a/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx +++ b/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx @@ -14,32 +14,35 @@ * limitations under the License. */ -import React, { useCallback, useState } from 'react'; -import { - Button, - CircularProgress, - Grid, - Link, - List, - ListItem, - Typography, - Divider, -} from '@material-ui/core'; -import { useGithubRepos } from '../util/useGithubRepos'; -import { ConfigSpec } from './ImportComponentPage'; +import { Entity } from '@backstage/catalog-model'; import { errorApiRef, RouteRef, StructuredMetadataTable, useApi, } from '@backstage/core'; -import { PartialEntity } from '../util/types'; +import { + entityRoute, + entityRouteParams, +} from '@backstage/plugin-catalog-common-react'; +import { + Button, + CircularProgress, + Divider, + Grid, + Link, + List, + ListItem, + Typography, +} from '@material-ui/core'; +import React, { useCallback, useState } from 'react'; import { generatePath, resolvePath } from 'react-router'; -import { entityRoute, entityRouteParams } from '@backstage/plugin-catalog'; -import { Entity } from '@backstage/catalog-model'; import { Link as RouterLink } from 'react-router-dom'; import * as YAML from 'yaml'; +import { PartialEntity } from '../util/types'; import { urlType } from '../util/urls'; +import { useGithubRepos } from '../util/useGithubRepos'; +import { ConfigSpec } from './ImportComponentPage'; const getEntityCatalogPath = ({ entity, diff --git a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx index 91a5208bee..d86db71f92 100644 --- a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx +++ b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx @@ -13,18 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { RegisterComponentForm } from './ImportComponentForm'; import { ApiProvider, ApiRegistry, DiscoveryApi, errorApiRef, } from '@backstage/core'; -import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog'; +import { + catalogApiRef, + CatalogClient, +} from '@backstage/plugin-catalog-common-react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; +import React from 'react'; import { catalogImportApiRef, CatalogImportClient } from '../api'; -import { fireEvent, waitFor, screen } from '@testing-library/react'; +import { RegisterComponentForm } from './ImportComponentForm'; describe('', () => { let apis: ApiRegistry; diff --git a/plugins/catalog-import/src/components/ImportComponentForm.tsx b/plugins/catalog-import/src/components/ImportComponentForm.tsx index c233be5a8c..25bea7fbe3 100644 --- a/plugins/catalog-import/src/components/ImportComponentForm.tsx +++ b/plugins/catalog-import/src/components/ImportComponentForm.tsx @@ -15,6 +15,7 @@ */ import { errorApiRef, useApi } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; import { BackstageTheme } from '@backstage/theme'; import { Button, @@ -26,11 +27,10 @@ import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; import { useForm } from 'react-hook-form'; import { useMountedState } from 'react-use'; -import { ComponentIdValidators } from '../util/validate'; -import { useGithubRepos } from '../util/useGithubRepos'; -import { ConfigSpec } from './ImportComponentPage'; -import { catalogApiRef } from '@backstage/plugin-catalog'; import { urlType } from '../util/urls'; +import { useGithubRepos } from '../util/useGithubRepos'; +import { ComponentIdValidators } from '../util/validate'; +import { ConfigSpec } from './ImportComponentPage'; const useStyles = makeStyles(theme => ({ form: { diff --git a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx index 932f9aafa0..9d7dd1eae1 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx @@ -13,22 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { msw, renderInTestApp } from '@backstage/test-utils'; -import { ImportComponentPage } from './ImportComponentPage'; import { ApiProvider, ApiRegistry, configApiRef, errorApiRef, } from '@backstage/core'; -import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog'; -import { catalogImportApiRef, CatalogImportClient } from '../api'; - +import { + catalogApiRef, + CatalogClient, +} from '@backstage/plugin-catalog-common-react'; +import { msw, renderInTestApp } from '@backstage/test-utils'; import { fireEvent, screen, waitFor } from '@testing-library/react'; - import { rest } from 'msw'; import { setupServer } from 'msw/node'; +import React from 'react'; +import { catalogImportApiRef, CatalogImportClient } from '../api'; +import { ImportComponentPage } from './ImportComponentPage'; let codeSearchMockResponse: () => Promise<{ data: { diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 90bdd2f3c7..0f250d5a41 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -33,6 +33,7 @@ "@backstage/catalog-client": "^0.3.5", "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", + "@backstage/plugin-catalog-common-react": "^0.0.1", "@backstage/plugin-scaffolder": "^0.4.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", @@ -68,4 +69,4 @@ "files": [ "dist" ] -} +} \ No newline at end of file diff --git a/plugins/catalog/src/components/AboutCard/AboutContent.tsx b/plugins/catalog/src/components/AboutCard/AboutContent.tsx index 82652b3986..22dfc3c2a3 100644 --- a/plugins/catalog/src/components/AboutCard/AboutContent.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutContent.tsx @@ -19,9 +19,9 @@ import { RELATION_OWNED_BY, RELATION_PART_OF, } from '@backstage/catalog-model'; +import { EntityRefLinks } from '@backstage/plugin-catalog-common-react'; import { Chip, Grid, makeStyles, Typography } from '@material-ui/core'; import React from 'react'; -import { EntityRefLinks } from '../EntityRefLink'; import { getEntityRelations } from '../getEntityRelations'; import { AboutField } from './AboutField'; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index afb812ed06..de280e7e1b 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -22,6 +22,7 @@ import { SupportButton, useApi, } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder'; import { Button, makeStyles } from '@material-ui/core'; import SettingsIcon from '@material-ui/icons/Settings'; @@ -30,14 +31,13 @@ import React, { useCallback, useMemo, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter'; import { useStarredEntities } from '../../hooks/useStarredEntities'; -import { catalogApiRef } from '../../plugin'; import { ButtonGroup, CatalogFilter } from '../CatalogFilter/CatalogFilter'; import { CatalogTable } from '../CatalogTable/CatalogTable'; +import { isOwnerOf } from '../isOwnerOf'; import { ResultsFilter } from '../ResultsFilter/ResultsFilter'; +import { useOwnUser } from '../useOwnUser'; import CatalogLayout from './CatalogLayout'; import { CatalogTabs, LabeledComponentType } from './CatalogTabs'; -import { useOwnUser } from '../useOwnUser'; -import { isOwnerOf } from '../isOwnerOf'; const useStyles = makeStyles(theme => ({ contentWrapper: { diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 18c61c2a04..52d0ab9178 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -20,6 +20,11 @@ import { RELATION_PART_OF, } from '@backstage/catalog-model'; import { Table, TableColumn, TableProps } from '@backstage/core'; +import { + EntityRefLink, + EntityRefLinks, + formatEntityRefTitle, +} from '@backstage/plugin-catalog-common-react'; import { Chip } from '@material-ui/core'; import Edit from '@material-ui/icons/Edit'; import OpenInNew from '@material-ui/icons/OpenInNew'; @@ -28,11 +33,6 @@ import React from 'react'; import { findLocationForEntityMeta } from '../../data/utils'; import { useStarredEntities } from '../../hooks/useStarredEntities'; import { createEditLink } from '../createEditLink'; -import { - EntityRefLink, - EntityRefLinks, - formatEntityRefTitle, -} from '../EntityRefLink'; import { favouriteEntityIcon, favouriteEntityTooltip, diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx index 5d1f010740..ba52c9a114 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx @@ -13,22 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { EntityLayout } from './EntityLayout'; +import { CatalogApi } from '@backstage/catalog-client'; +import { Entity } from '@backstage/catalog-model'; import { AlertApi, alertApiRef, ApiProvider, ApiRegistry, } from '@backstage/core'; +import { + catalogApiRef, + EntityContext, +} from '@backstage/plugin-catalog-common-react'; import { renderInTestApp, withLogCollector } from '@backstage/test-utils'; import { fireEvent } from '@testing-library/react'; +import React from 'react'; import { act } from 'react-dom/test-utils'; -import { Routes, Route } from 'react-router'; -import { Entity } from '@backstage/catalog-model'; -import { EntityContext } from '../../hooks/useEntity'; -import { catalogApiRef } from '../../plugin'; -import { CatalogApi } from '@backstage/catalog-client'; +import { Route, Routes } from 'react-router'; +import { EntityLayout } from './EntityLayout'; const mockEntityData = { loading: false, diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index 09b8d02c60..24efdebb96 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -14,15 +14,6 @@ * limitations under the License. */ -import React, { - Children, - Fragment, - PropsWithChildren, - ReactNode, - isValidElement, - useContext, - useState, -} from 'react'; import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { attachComponentData, @@ -32,14 +23,25 @@ import { Page, Progress, } from '@backstage/core'; +import { + EntityContext, + useEntityCompoundName, +} from '@backstage/plugin-catalog-common-react'; import { Box } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; +import React, { + Children, + Fragment, + isValidElement, + PropsWithChildren, + ReactNode, + useContext, + useState, +} from 'react'; import { useNavigate } from 'react-router'; -import { EntityContext } from '../../hooks/useEntity'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog'; -import { useEntityCompoundName } from '../useEntityCompoundName'; import { TabbedLayout } from './TabbedLayout'; type SubRoute = { diff --git a/plugins/catalog/src/components/EntityLoaderProvider/EntityLoaderProvider.tsx b/plugins/catalog/src/components/EntityLoaderProvider/EntityLoaderProvider.tsx index 98b8295047..555595e818 100644 --- a/plugins/catalog/src/components/EntityLoaderProvider/EntityLoaderProvider.tsx +++ b/plugins/catalog/src/components/EntityLoaderProvider/EntityLoaderProvider.tsx @@ -13,8 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { + EntityContext, + useEntityFromUrl, +} from '@backstage/plugin-catalog-common-react'; import React, { ReactNode } from 'react'; -import { useEntityFromUrl, EntityContext } from '../../hooks/useEntity'; export const EntityLoaderProvider = ({ children }: { children: ReactNode }) => { const { entity, loading, error } = useEntityFromUrl(); diff --git a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx index eb75593ffc..cb4f9bc644 100644 --- a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx +++ b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx @@ -19,11 +19,14 @@ import { Box } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; import React, { PropsWithChildren, useContext, useState } from 'react'; import { useNavigate } from 'react-router'; -import { EntityContext } from '../../hooks/useEntity'; +import { + EntityContext, + useEntityCompoundName, +} from '@backstage/plugin-catalog-common-react'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog'; -import { useEntityCompoundName } from '../useEntityCompoundName'; + import { Tabbed } from './Tabbed'; const EntityPageTitle = ({ diff --git a/plugins/catalog/src/components/EntityProvider/EntityProvider.tsx b/plugins/catalog/src/components/EntityProvider/EntityProvider.tsx index 9135b54638..836e2aa752 100644 --- a/plugins/catalog/src/components/EntityProvider/EntityProvider.tsx +++ b/plugins/catalog/src/components/EntityProvider/EntityProvider.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; import React, { ReactNode } from 'react'; -import { EntityContext } from '../../hooks/useEntity'; +import { EntityContext } from '@backstage/plugin-catalog-common-react'; type EntityProviderProps = { entity: Entity; diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx index 292cd37ede..4be4713075 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx @@ -14,12 +14,12 @@ * limitations under the License. */ -import React from 'react'; -import { render } from '@testing-library/react'; -import { EntityContext } from '../../hooks/useEntity'; import { Entity } from '@backstage/catalog-model'; -import { EntitySwitch } from './EntitySwitch'; +import { EntityContext } from '@backstage/plugin-catalog-common-react'; +import { render } from '@testing-library/react'; +import React from 'react'; import { isKind } from './conditions'; +import { EntitySwitch } from './EntitySwitch'; describe('EntitySwitch', () => { it('should switch child when entity switches', () => { diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx index f36bc29cee..4312fba5db 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx @@ -14,16 +14,16 @@ * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-common-react'; import { - ReactNode, - PropsWithChildren, Children, Fragment, - useMemo, isValidElement, + PropsWithChildren, + ReactNode, + useMemo, } from 'react'; -import { useEntity } from '../../hooks/useEntity'; -import { Entity } from '@backstage/catalog-model'; const EntitySwitchCase = (_: { if?: (entity: Entity) => boolean; diff --git a/plugins/catalog/src/components/Router.tsx b/plugins/catalog/src/components/Router.tsx index 6a4593fb80..b54dfef01c 100644 --- a/plugins/catalog/src/components/Router.tsx +++ b/plugins/catalog/src/components/Router.tsx @@ -15,15 +15,18 @@ */ import { ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { Content } from '@backstage/core'; +import { + entityRoute, + rootRoute, + useEntity, +} from '@backstage/plugin-catalog-common-react'; import { Link, Typography } from '@material-ui/core'; import React, { ComponentType } from 'react'; import { Navigate, Route, Routes, useParams } from 'react-router'; -import { useEntity } from '../hooks/useEntity'; -import { entityRoute, rootRoute } from '../routes'; import { CatalogPage } from './CatalogPage'; +import { EntityLoaderProvider } from './EntityLoaderProvider'; import { EntityNotFound } from './EntityNotFound'; import { EntityPageLayout } from './EntityPageLayout'; -import { EntityLoaderProvider } from './EntityLoaderProvider'; const DefaultEntityPage = () => ( diff --git a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx b/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx index 2c8fcfd6e9..5e4212ee1b 100644 --- a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx +++ b/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx @@ -16,6 +16,10 @@ import { Entity, ORIGIN_LOCATION_ANNOTATION } from '@backstage/catalog-model'; import { alertApiRef, configApiRef, Progress, useApi } from '@backstage/core'; +import { + catalogApiRef, + formatEntityRefTitle, +} from '@backstage/plugin-catalog-common-react'; import { Button, Dialog, @@ -31,8 +35,6 @@ import Alert from '@material-ui/lab/Alert'; import React from 'react'; import { useAsync } from 'react-use'; import { AsyncState } from 'react-use/lib/useAsync'; -import { catalogApiRef } from '../../plugin'; -import { formatEntityRefTitle } from '../EntityRefLink'; type Props = { open: boolean; diff --git a/plugins/catalog/src/components/useOwnUser.ts b/plugins/catalog/src/components/useOwnUser.ts index 9aa4bda99c..43d658e8d0 100644 --- a/plugins/catalog/src/components/useOwnUser.ts +++ b/plugins/catalog/src/components/useOwnUser.ts @@ -15,10 +15,10 @@ */ import { UserEntity } from '@backstage/catalog-model'; +import { identityApiRef, useApi } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; import { useAsync } from 'react-use'; import { AsyncState } from 'react-use/lib/useAsync'; -import { identityApiRef, useApi } from '@backstage/core'; -import { catalogApiRef } from '../plugin'; /** * Get the catalog User entity (if any) that matches the logged-in user. diff --git a/plugins/catalog/src/extensions.tsx b/plugins/catalog/src/extensions.tsx index dcb9ba2aff..2a214dde23 100644 --- a/plugins/catalog/src/extensions.tsx +++ b/plugins/catalog/src/extensions.tsx @@ -15,7 +15,10 @@ */ import { createRoutableExtension } from '@backstage/core'; -import { catalogRouteRef, entityRouteRef } from './routes'; +import { + catalogRouteRef, + entityRouteRef, +} from '@backstage/plugin-catalog-common-react'; import { plugin } from './plugin'; export const CatalogIndexPage = plugin.provide( diff --git a/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx b/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx index edda8eecb5..2d954fb2d2 100644 --- a/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx +++ b/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx @@ -16,9 +16,9 @@ import { Entity } from '@backstage/catalog-model'; import { useApi } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useAsyncFn } from 'react-use'; -import { catalogApiRef } from '../plugin'; import { filterGroupsContext, FilterGroupsContext } from './context'; import { EntityFilterFn, diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 2152933daf..5a93b80539 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -14,15 +14,11 @@ * limitations under the License. */ -export * from '@backstage/catalog-client'; export { AboutCard } from './components/AboutCard'; -export { EntityPageLayout } from './components/EntityPageLayout'; export { EntityLayout } from './components/EntityLayout'; +export { EntityPageLayout } from './components/EntityPageLayout'; export { EntityProvider } from './components/EntityProvider'; export * from './components/EntitySwitch'; export { Router } from './components/Router'; -export { useEntityCompoundName } from './components/useEntityCompoundName'; -export { EntityContext, useEntity } from './hooks/useEntity'; -export { catalogApiRef, plugin } from './plugin'; -export * from './routes'; export * from './extensions'; +export { plugin } from './plugin'; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 0c2b3b18c5..be57a2e9f9 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -14,20 +14,17 @@ * limitations under the License. */ -import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; +import { CatalogClient } from '@backstage/catalog-client'; import { createApiFactory, - createApiRef, createPlugin, discoveryApiRef, } from '@backstage/core'; -import { catalogRouteRef, entityRouteRef } from './routes'; - -export const catalogApiRef = createApiRef({ - id: 'plugin.catalog.service', - description: - 'Used by the Catalog plugin to make requests to accompanying backend', -}); +import { + catalogApiRef, + catalogRouteRef, + entityRouteRef, +} from '@backstage/plugin-catalog-common-react'; export const plugin = createPlugin({ id: 'catalog', diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index ddae98a566..b731146e40 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -33,7 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.14", + "@backstage/plugin-catalog-common-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/circleci/src/state/useBuilds.ts b/plugins/circleci/src/state/useBuilds.ts index c172a88985..b7143f1cb5 100644 --- a/plugins/circleci/src/state/useBuilds.ts +++ b/plugins/circleci/src/state/useBuilds.ts @@ -15,14 +15,14 @@ */ import { errorApiRef, useApi } from '@backstage/core'; +import { useEntity } from '@backstage/plugin-catalog-common-react'; import { BuildSummary, GitType } from 'circleci-api'; +import { getOr } from 'lodash/fp'; import { useCallback, useEffect, useState } from 'react'; import { useAsyncRetry } from 'react-use'; import { circleCIApiRef } from '../api'; import type { CITableBuildInfo } from '../components/BuildsPage/lib/CITable'; -import { useEntity } from '@backstage/plugin-catalog'; import { CIRCLECI_ANNOTATION } from '../constants'; -import { getOr } from 'lodash/fp'; const makeReadableStatus = (status: string | undefined) => { if (!status) return ''; diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 9a05a652b9..3e5bbef3a9 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -32,7 +32,6 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.14", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 7b7cf539f9..beb35b3cb0 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -35,7 +35,6 @@ "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", "@backstage/integration": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.14", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index a99b9937b1..75dec65ee2 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -33,7 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.14", + "@backstage/plugin-catalog-common-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/jenkins/src/components/useProjectSlugFromEntity.ts b/plugins/jenkins/src/components/useProjectSlugFromEntity.ts index 1dca918990..2c5f138402 100644 --- a/plugins/jenkins/src/components/useProjectSlugFromEntity.ts +++ b/plugins/jenkins/src/components/useProjectSlugFromEntity.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useEntity } from '@backstage/plugin-catalog'; +import { useEntity } from '@backstage/plugin-catalog-common-react'; import { JENKINS_ANNOTATION } from '../constants'; export const useProjectSlugFromEntity = () => { diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index eef14439fd..03f602c049 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -22,7 +22,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.14", + "@backstage/plugin-catalog-common-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx index 3bee4e2e88..595905d7a3 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { PropsWithChildren } from 'react'; -import { renderHook } from '@testing-library/react-hooks'; -import { useConsumerGroupsForEntity } from './useConsumerGroupsForEntity'; -import { EntityContext } from '@backstage/plugin-catalog'; import { Entity } from '@backstage/catalog-model'; +import { EntityContext } from '@backstage/plugin-catalog-common-react'; +import { renderHook } from '@testing-library/react-hooks'; +import React, { PropsWithChildren } from 'react'; +import { useConsumerGroupsForEntity } from './useConsumerGroupsForEntity'; describe('useConsumerGroupOffsets', () => { let entity: Entity; diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts index 522a6273fe..c81573e2ff 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { useEntity } from '@backstage/plugin-catalog'; +import { useEntity } from '@backstage/plugin-catalog-common-react'; import { useMemo } from 'react'; import { KAFKA_CONSUMER_GROUP_ANNOTATION } from '../../constants'; diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx index b7e3a29e80..0d9846dba5 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx @@ -13,19 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { PropsWithChildren } from 'react'; -import { renderHook } from '@testing-library/react-hooks'; -import { EntityContext } from '@backstage/plugin-catalog'; import { Entity } from '@backstage/catalog-model'; -import * as data from './__fixtures__/consumer-group-offsets.json'; +import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; +import { EntityContext } from '@backstage/plugin-catalog-common-react'; +import { renderHook } from '@testing-library/react-hooks'; +import { when } from 'jest-when'; +import React, { PropsWithChildren } from 'react'; import { ConsumerGroupOffsetsResponse, KafkaApi, kafkaApiRef, } from '../../api/types'; -import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; import { useConsumerGroupsOffsetsForEntity } from './useConsumerGroupsOffsetsForEntity'; -import { when } from 'jest-when'; +import * as data from './__fixtures__/consumer-group-offsets.json'; const consumerGroupOffsets = data as ConsumerGroupOffsetsResponse; diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 3463842bc5..c1df09ac1d 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -34,7 +34,7 @@ "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.14", + "@backstage/plugin-catalog-common-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx index 15a4ba3255..cd4f1f9e5f 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx @@ -14,23 +14,22 @@ * limitations under the License. */ -import React from 'react'; -import { render } from '@testing-library/react'; +import { Entity } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; +import { EntityContext } from '@backstage/plugin-catalog-common-react'; +import { lightTheme } from '@backstage/theme'; +import { ThemeProvider } from '@material-ui/core'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { lighthouseApiRef, LighthouseRestApi, WebsiteListResponse, } from '../../api'; - -import * as data from '../../__fixtures__/website-list-response.json'; -import { EntityContext } from '@backstage/plugin-catalog'; -import { Entity } from '@backstage/catalog-model'; -import { AuditListForEntity } from './AuditListForEntity'; -import { lightTheme } from '@backstage/theme'; -import { ThemeProvider } from '@material-ui/core'; -import { MemoryRouter } from 'react-router-dom'; import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; +import * as data from '../../__fixtures__/website-list-response.json'; +import { AuditListForEntity } from './AuditListForEntity'; jest.mock('../../hooks/useWebsiteForEntity', () => ({ useWebsiteForEntity: jest.fn(), diff --git a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx index 42c213b42e..c9405a25c3 100644 --- a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx +++ b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx @@ -14,21 +14,21 @@ * limitations under the License. */ -import React from 'react'; +import { Entity } from '@backstage/catalog-model'; +import { EntityContext } from '@backstage/plugin-catalog-common-react'; +import { lightTheme } from '@backstage/theme'; +import { ThemeProvider } from '@material-ui/core'; import { render } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { AuditCompleted, LighthouseCategoryId, WebsiteListResponse, } from '../../api'; -import { EntityContext } from '@backstage/plugin-catalog'; -import { Entity } from '@backstage/catalog-model'; -import { LastLighthouseAuditCard } from './LastLighthouseAuditCard'; -import { lightTheme } from '@backstage/theme'; -import { ThemeProvider } from '@material-ui/core'; import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; -import { MemoryRouter } from 'react-router-dom'; import * as data from '../../__fixtures__/website-list-response.json'; +import { LastLighthouseAuditCard } from './LastLighthouseAuditCard'; jest.mock('../../hooks/useWebsiteForEntity', () => ({ useWebsiteForEntity: jest.fn(), diff --git a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx index c1773921cf..aa9c9c3557 100644 --- a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx +++ b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { PropsWithChildren } from 'react'; -import { renderHook } from '@testing-library/react-hooks'; -import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; -import { lighthouseApiRef, WebsiteListResponse } from '../api'; -import { useWebsiteForEntity } from './useWebsiteForEntity'; -import { EntityContext } from '@backstage/plugin-catalog'; import { Entity } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; +import { EntityContext } from '@backstage/plugin-catalog-common-react'; +import { renderHook } from '@testing-library/react-hooks'; +import React, { PropsWithChildren } from 'react'; +import { lighthouseApiRef, WebsiteListResponse } from '../api'; import * as data from '../__fixtures__/website-list-response.json'; +import { useWebsiteForEntity } from './useWebsiteForEntity'; const websiteListResponse = data as WebsiteListResponse; const website = websiteListResponse.items[0]; diff --git a/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts b/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts index 08d9925b7a..da351a6c82 100644 --- a/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts +++ b/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useEntity } from '@backstage/plugin-catalog'; -import { LIGHTHOUSE_WEBSITE_URL_ANNOTATION } from '../../constants'; import { errorApiRef, useApi } from '@backstage/core'; -import { lighthouseApiRef } from '../api'; +import { useEntity } from '@backstage/plugin-catalog-common-react'; import { useAsync } from 'react-use'; +import { LIGHTHOUSE_WEBSITE_URL_ANNOTATION } from '../../constants'; +import { lighthouseApiRef } from '../api'; // For the sake of simplicity we assume that an entity has only one website url. This is to avoid encoding a list // type in an annotation which is a plain string. diff --git a/plugins/org/package.json b/plugins/org/package.json index fbb3bd1afb..7cc55dbf60 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -22,7 +22,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.14", + "@backstage/plugin-catalog-common-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index 779cb1d92d..0238fea7f6 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -21,7 +21,7 @@ import { RELATION_PARENT_OF, } from '@backstage/catalog-model'; import { Avatar, InfoCard } from '@backstage/core'; -import { entityRouteParams } from '@backstage/plugin-catalog'; +import { entityRouteParams } from '@backstage/plugin-catalog-common-react'; import { Box, Grid, Link, Tooltip, Typography } from '@material-ui/core'; import AccountTreeIcon from '@material-ui/icons/AccountTree'; import EmailIcon from '@material-ui/icons/Email'; diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx index 535b2acd0e..59260f93fc 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx @@ -14,11 +14,14 @@ * limitations under the License. */ +import { Entity, GroupEntity } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { + CatalogApi, + catalogApiRef, +} from '@backstage/plugin-catalog-common-react'; import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; import React from 'react'; -import { ApiProvider, ApiRegistry } from '@backstage/core'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; -import { Entity, GroupEntity } from '@backstage/catalog-model'; import { MembersListCard } from './MembersListCard'; describe('MemberTab Test', () => { diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx index 04a137d515..cd001d6e96 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -20,7 +20,10 @@ import { UserEntity, } from '@backstage/catalog-model'; import { Avatar, InfoCard, Progress, useApi } from '@backstage/core'; -import { catalogApiRef, entityRouteParams } from '@backstage/plugin-catalog'; +import { + catalogApiRef, + entityRouteParams, +} from '@backstage/plugin-catalog-common-react'; import { Box, createStyles, diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx index a61dd55161..3d459d7cb4 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { InfoCard, useApi, Progress } from '@backstage/core'; import { Entity } from '@backstage/catalog-model'; -import { catalogApiRef } from '@backstage/plugin-catalog'; +import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; import { useAsync } from 'react-use'; import Alert from '@material-ui/lab/Alert'; import { diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx index 99048c0cd8..f1897efed1 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -19,7 +19,7 @@ import { UserEntity, } from '@backstage/catalog-model'; import { Avatar, InfoCard } from '@backstage/core'; -import { entityRouteParams } from '@backstage/plugin-catalog'; +import { entityRouteParams } from '@backstage/plugin-catalog-common-react'; import { Box, Grid, Link, Tooltip, Typography } from '@material-ui/core'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 29b629de9b..e12a27529f 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -32,7 +32,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.14", + "@backstage/plugin-catalog-common-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx index 6b097085e9..c2d52f25d9 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx @@ -20,7 +20,7 @@ import { createRouteRef, errorApiRef, } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog'; +import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; import { render, screen } from '@testing-library/react'; diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx index d0b56598b2..651af14723 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx @@ -14,23 +14,23 @@ * limitations under the License. */ -import React, { useState } from 'react'; -import { Grid, makeStyles } from '@material-ui/core'; +import { Entity, Location } from '@backstage/catalog-model'; import { - InfoCard, - Page, Content, - useApi, + ContentHeader, errorApiRef, Header, - SupportButton, - ContentHeader, + InfoCard, + Page, RouteRef, + SupportButton, + useApi, } from '@backstage/core'; -import { RegisterComponentForm } from '../RegisterComponentForm'; -import { catalogApiRef } from '@backstage/plugin-catalog'; +import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; +import { Grid, makeStyles } from '@material-ui/core'; +import React, { useState } from 'react'; import { useMountedState } from 'react-use'; -import { Entity, Location } from '@backstage/catalog-model'; +import { RegisterComponentForm } from '../RegisterComponentForm'; import { RegisterComponentResultDialog } from '../RegisterComponentResultDialog'; const useStyles = makeStyles(theme => ({ diff --git a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx index c31d1344cb..aef07c8172 100644 --- a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx +++ b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx @@ -16,7 +16,10 @@ import { Entity } from '@backstage/catalog-model'; import { RouteRef, StructuredMetadataTable } from '@backstage/core'; -import { entityRoute, entityRouteParams } from '@backstage/plugin-catalog'; +import { + entityRoute, + entityRouteParams, +} from '@backstage/plugin-catalog-common-react'; import { Button, Dialog, diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index f3a21709db..fbb5994dcd 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -33,7 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.14", + "@backstage/plugin-catalog-common-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/rollbar/src/components/RollbarHome/RollbarHome.test.tsx b/plugins/rollbar/src/components/RollbarHome/RollbarHome.test.tsx index ff3e909bf7..cb42be7e32 100644 --- a/plugins/rollbar/src/components/RollbarHome/RollbarHome.test.tsx +++ b/plugins/rollbar/src/components/RollbarHome/RollbarHome.test.tsx @@ -21,7 +21,10 @@ import { ConfigApi, configApiRef, } from '@backstage/core'; -import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog'; +import { + catalogApiRef, + CatalogApi, +} from '@backstage/plugin-catalog-common-react'; import { wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi'; diff --git a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx index a1c54dfd5a..b6e6126b27 100644 --- a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx +++ b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx @@ -14,19 +14,22 @@ * limitations under the License. */ -import * as React from 'react'; import { ApiProvider, ApiRegistry, ConfigApi, configApiRef, } from '@backstage/core'; +import { + CatalogApi, + catalogApiRef, +} from '@backstage/plugin-catalog-common-react'; import { wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; +import * as React from 'react'; import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi'; import { RollbarTopActiveItem } from '../../api/types'; import { RollbarProjectPage } from './RollbarProjectPage'; -import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog'; describe('RollbarProjectPage component', () => { const items: RollbarTopActiveItem[] = [ diff --git a/plugins/rollbar/src/hooks/useCatalogEntity.ts b/plugins/rollbar/src/hooks/useCatalogEntity.ts index 172d057738..3d539f19cc 100644 --- a/plugins/rollbar/src/hooks/useCatalogEntity.ts +++ b/plugins/rollbar/src/hooks/useCatalogEntity.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { useAsync } from 'react-use'; import { useApi } from '@backstage/core'; import { catalogApiRef, useEntityCompoundName, -} from '@backstage/plugin-catalog'; +} from '@backstage/plugin-catalog-common-react'; +import { useAsync } from 'react-use'; export function useCatalogEntity() { const catalogApi = useApi(catalogApiRef); diff --git a/plugins/rollbar/src/hooks/useRollbarEntities.ts b/plugins/rollbar/src/hooks/useRollbarEntities.ts index 5979c5832a..24e2d100bb 100644 --- a/plugins/rollbar/src/hooks/useRollbarEntities.ts +++ b/plugins/rollbar/src/hooks/useRollbarEntities.ts @@ -14,9 +14,9 @@ * limitations under the License. */ +import { configApiRef, useApi } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; import { useAsync } from 'react-use'; -import { useApi, configApiRef } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog'; import { ROLLBAR_ANNOTATION } from '../constants'; export function useRollbarEntities() { diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index d81d176232..ded0544d3a 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -32,7 +32,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.14", + "@backstage/plugin-catalog-common-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 39999f938f..6178877330 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -27,7 +27,7 @@ import { useApi, WarningPanel, } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog'; +import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; import { Button, Grid, Link, Typography } from '@material-ui/core'; import React, { useEffect } from 'react'; import { Link as RouterLink } from 'react-router-dom'; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx index 62963b58b8..793895e699 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx @@ -14,7 +14,10 @@ * limitations under the License. */ import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; +import { + CatalogApi, + catalogApiRef, +} from '@backstage/plugin-catalog-common-react'; import { renderInTestApp, renderWithEffects } from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 60f5ad8dec..360abd441c 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -27,19 +27,19 @@ import { catalogApiRef, entityRoute, entityRouteParams, -} from '@backstage/plugin-catalog'; +} from '@backstage/plugin-catalog-common-react'; import { LinearProgress } from '@material-ui/core'; import { IChangeEvent } from '@rjsf/core'; -import React, { useState, useCallback } from 'react'; +import parseGitUrl from 'git-url-parse'; +import React, { useCallback, useState } from 'react'; import { generatePath, Navigate } from 'react-router'; import { useParams } from 'react-router-dom'; import { useAsync } from 'react-use'; import { scaffolderApiRef } from '../../api'; import { rootRoute } from '../../routes'; +import { useJobPolling } from '../hooks/useJobPolling'; import { JobStatusModal } from '../JobStatusModal'; import { MultistepJsonForm } from '../MultistepJsonForm'; -import { useJobPolling } from '../hooks/useJobPolling'; -import parseGitUrl from 'git-url-parse'; const useTemplate = ( templateName: string, diff --git a/plugins/search/package.json b/plugins/search/package.json index 9409a416a0..1185646d67 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -30,8 +30,8 @@ }, "dependencies": { "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.14", "@backstage/catalog-model": "^0.7.0", + "@backstage/plugin-catalog-common-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/search/src/apis.ts b/plugins/search/src/apis.ts index e74741e27b..34793777a8 100644 --- a/plugins/search/src/apis.ts +++ b/plugins/search/src/apis.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { CatalogApi } from '@backstage/plugin-catalog'; import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { CatalogApi } from '@backstage/plugin-catalog-common-react'; export type Result = { name: string; diff --git a/plugins/search/src/components/SearchResult/SearchResult.tsx b/plugins/search/src/components/SearchResult/SearchResult.tsx index f81cd4f66d..72e592302c 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.tsx @@ -13,23 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useState, useEffect } from 'react'; -import { useAsync } from 'react-use'; - -import { makeStyles, Typography, Grid, Divider } from '@material-ui/core'; -import { Alert } from '@material-ui/lab'; import { - Link, EmptyState, + Link, Progress, Table, TableColumn, useApi, } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog'; - -import { FiltersButton, Filters, FiltersState } from '../Filters'; +import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; +import { Divider, Grid, makeStyles, Typography } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import React, { useEffect, useState } from 'react'; +import { useAsync } from 'react-use'; import SearchApi, { Result, SearchResults } from '../../apis'; +import { Filters, FiltersButton, FiltersState } from '../Filters'; const useStyles = makeStyles(theme => ({ searchQuery: { diff --git a/plugins/sentry/dev/index.tsx b/plugins/sentry/dev/index.tsx index c9313911b2..98b687b4bf 100644 --- a/plugins/sentry/dev/index.tsx +++ b/plugins/sentry/dev/index.tsx @@ -15,17 +15,17 @@ */ import { Entity } from '@backstage/catalog-model'; -import { EntityProvider } from '@backstage/plugin-catalog'; import { Content, Header, Page } from '@backstage/core'; import { createDevApp, EntityGridItem } from '@backstage/dev-utils'; +import { EntityProvider } from '@backstage/plugin-catalog-common-react'; import { Grid } from '@material-ui/core'; import React from 'react'; import { + EntitySentryCard, + EntitySentryContent, MockSentryApi, SentryApi, sentryApiRef, - EntitySentryCard, - EntitySentryContent, } from '../src'; import { SENTRY_PROJECT_SLUG_ANNOTATION } from '../src/components/useProjectSlug'; diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 52343ad332..2d5edaabb5 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -33,7 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.14", + "@backstage/plugin-catalog-common-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/sentry/src/extensions.tsx b/plugins/sentry/src/extensions.tsx index a66080f2b6..b606beed37 100644 --- a/plugins/sentry/src/extensions.tsx +++ b/plugins/sentry/src/extensions.tsx @@ -14,12 +14,12 @@ * limitations under the License. */ -import React from 'react'; import { createComponentExtension, createRoutableExtension, } from '@backstage/core'; -import { useEntity } from '@backstage/plugin-catalog'; +import { useEntity } from '@backstage/plugin-catalog-common-react'; +import React from 'react'; import { plugin, rootRouteRef } from './plugin'; export const EntitySentryContent = plugin.provide( diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 2c1e0e5911..5bd8ac0d9d 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -33,7 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.14", + "@backstage/plugin-catalog-common-react": "^0.0.1", "@backstage/test-utils": "^0.1.6", "@backstage/theme": "^0.2.2", "@backstage/techdocs-common": "^0.3.6", diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx index 4979f027a6..bfca7aefe3 100644 --- a/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx @@ -15,7 +15,10 @@ */ import { ApiProvider, ApiRegistry } from '@backstage/core'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; +import { + CatalogApi, + catalogApiRef, +} from '@backstage/plugin-catalog-common-react'; import { wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import React from 'react'; diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.tsx index 9f569dd167..63dd0dceae 100644 --- a/plugins/techdocs/src/reader/components/TechDocsHome.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsHome.tsx @@ -22,7 +22,7 @@ import { Progress, useApi, } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog'; +import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; import { Grid } from '@material-ui/core'; import React from 'react'; import { generatePath, useNavigate } from 'react-router-dom'; From c677643afa8ecd48aa630493d8523ec41fc29359 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 26 Jan 2021 19:24:04 +0100 Subject: [PATCH 34/99] Rename from @backstage/plugin-catalog-common-react to @backstage/plugin-catalog-react --- .changeset/wet-suits-live.md | 2 +- .changeset/wild-cows-exercise.md | 2 +- packages/app/package.json | 2 +- packages/app/src/components/catalog/EntityPage.tsx | 2 +- packages/create-app/src/lib/versions.ts | 4 ++-- .../templates/default-app/packages/app/package.json.hbs | 2 +- .../packages/app/src/components/catalog/EntityPage.tsx | 2 +- plugins/api-docs/package.json | 2 +- .../src/components/ApiExplorerPage/ApiExplorerPage.test.tsx | 5 +---- .../src/components/ApiExplorerPage/ApiExplorerPage.tsx | 2 +- .../src/components/ApisCards/ConsumedApisCard.test.tsx | 5 +---- .../src/components/ApisCards/ProvidedApisCard.test.tsx | 5 +---- .../ComponentsCards/ConsumingComponentsCard.test.tsx | 5 +---- .../ComponentsCards/ProvidingComponentsCard.test.tsx | 5 +---- plugins/api-docs/src/components/EntityLink/EntityLink.tsx | 2 +- plugins/api-docs/src/components/useRelatedEntities.ts | 2 +- plugins/catalog-import/package.json | 2 +- .../catalog-import/src/components/ComponentConfigDisplay.tsx | 2 +- .../src/components/ImportComponentForm.test.tsx | 5 +---- .../catalog-import/src/components/ImportComponentForm.tsx | 2 +- .../src/components/ImportComponentPage.test.tsx | 5 +---- plugins/{catalog-common-react => catalog-react}/.eslintrc.js | 0 plugins/{catalog-common-react => catalog-react}/README.md | 0 plugins/{catalog-common-react => catalog-react}/package.json | 2 +- plugins/{catalog-common-react => catalog-react}/src/api.ts | 0 .../src/components/EntityRefLink/EntityRefLink.test.tsx | 0 .../src/components/EntityRefLink/EntityRefLink.tsx | 0 .../src/components/EntityRefLink/EntityRefLinks.test.tsx | 0 .../src/components/EntityRefLink/EntityRefLinks.tsx | 0 .../src/components/EntityRefLink/format.test.ts | 0 .../src/components/EntityRefLink/format.ts | 0 .../src/components/EntityRefLink/index.ts | 0 .../src/components/index.ts | 0 .../src/hooks/index.ts | 0 .../src/hooks/useEntity.ts | 0 .../src/hooks/useEntityCompoundName.ts | 0 plugins/{catalog-common-react => catalog-react}/src/index.ts | 0 .../{catalog-common-react => catalog-react}/src/routes.ts | 0 .../src/setupTests.ts | 0 plugins/catalog/package.json | 2 +- plugins/catalog/src/components/AboutCard/AboutContent.tsx | 2 +- plugins/catalog/src/components/CatalogPage/CatalogPage.tsx | 2 +- plugins/catalog/src/components/CatalogTable/CatalogTable.tsx | 2 +- .../src/components/EntityLayout/EntityLayout.test.tsx | 5 +---- plugins/catalog/src/components/EntityLayout/EntityLayout.tsx | 2 +- .../components/EntityLoaderProvider/EntityLoaderProvider.tsx | 2 +- .../src/components/EntityPageLayout/EntityPageLayout.tsx | 2 +- .../catalog/src/components/EntityProvider/EntityProvider.tsx | 2 +- .../src/components/EntitySwitch/EntitySwitch.test.tsx | 2 +- plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx | 2 +- plugins/catalog/src/components/Router.tsx | 2 +- .../UnregisterEntityDialog/UnregisterEntityDialog.tsx | 2 +- plugins/catalog/src/components/useOwnUser.ts | 2 +- plugins/catalog/src/extensions.tsx | 2 +- plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx | 2 +- plugins/catalog/src/plugin.ts | 2 +- plugins/circleci/package.json | 2 +- plugins/circleci/src/state/useBuilds.ts | 2 +- plugins/jenkins/package.json | 2 +- plugins/jenkins/src/components/useProjectSlugFromEntity.ts | 2 +- plugins/kafka/package.json | 2 +- .../ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx | 2 +- .../ConsumerGroupOffsets/useConsumerGroupsForEntity.ts | 2 +- .../useConsumerGroupsOffsetsForEntity.test.tsx | 2 +- plugins/lighthouse/package.json | 2 +- .../src/components/AuditList/AuditListForEntity.test.tsx | 2 +- .../src/components/Cards/LastLighthouseAuditCard.test.tsx | 2 +- plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx | 2 +- plugins/lighthouse/src/hooks/useWebsiteForEntity.ts | 2 +- plugins/org/package.json | 2 +- .../components/Cards/Group/GroupProfile/GroupProfileCard.tsx | 2 +- .../Cards/Group/MembersList/MembersListCard.test.tsx | 5 +---- .../components/Cards/Group/MembersList/MembersListCard.tsx | 2 +- .../org/src/components/Cards/OwnershipCard/OwnershipCard.tsx | 2 +- .../Cards/User/UserProfileCard/UserProfileCard.tsx | 2 +- plugins/register-component/package.json | 2 +- .../RegisterComponentPage/RegisterComponentPage.test.tsx | 2 +- .../RegisterComponentPage/RegisterComponentPage.tsx | 2 +- .../RegisterComponentResultDialog.tsx | 2 +- plugins/rollbar/package.json | 2 +- .../rollbar/src/components/RollbarHome/RollbarHome.test.tsx | 5 +---- .../RollbarProjectPage/RollbarProjectPage.test.tsx | 5 +---- plugins/rollbar/src/hooks/useCatalogEntity.ts | 2 +- plugins/rollbar/src/hooks/useRollbarEntities.ts | 2 +- plugins/scaffolder/package.json | 2 +- .../src/components/ScaffolderPage/ScaffolderPage.tsx | 2 +- .../src/components/TemplatePage/TemplatePage.test.tsx | 5 +---- .../scaffolder/src/components/TemplatePage/TemplatePage.tsx | 2 +- plugins/search/package.json | 2 +- plugins/search/src/apis.ts | 2 +- plugins/search/src/components/SearchResult/SearchResult.tsx | 2 +- plugins/sentry/dev/index.tsx | 2 +- plugins/sentry/package.json | 2 +- plugins/sentry/src/extensions.tsx | 2 +- plugins/techdocs/package.json | 2 +- plugins/techdocs/src/reader/components/TechDocsHome.test.tsx | 5 +---- plugins/techdocs/src/reader/components/TechDocsHome.tsx | 2 +- 97 files changed, 81 insertions(+), 120 deletions(-) rename plugins/{catalog-common-react => catalog-react}/.eslintrc.js (100%) rename plugins/{catalog-common-react => catalog-react}/README.md (100%) rename plugins/{catalog-common-react => catalog-react}/package.json (96%) rename plugins/{catalog-common-react => catalog-react}/src/api.ts (100%) rename plugins/{catalog-common-react => catalog-react}/src/components/EntityRefLink/EntityRefLink.test.tsx (100%) rename plugins/{catalog-common-react => catalog-react}/src/components/EntityRefLink/EntityRefLink.tsx (100%) rename plugins/{catalog-common-react => catalog-react}/src/components/EntityRefLink/EntityRefLinks.test.tsx (100%) rename plugins/{catalog-common-react => catalog-react}/src/components/EntityRefLink/EntityRefLinks.tsx (100%) rename plugins/{catalog-common-react => catalog-react}/src/components/EntityRefLink/format.test.ts (100%) rename plugins/{catalog-common-react => catalog-react}/src/components/EntityRefLink/format.ts (100%) rename plugins/{catalog-common-react => catalog-react}/src/components/EntityRefLink/index.ts (100%) rename plugins/{catalog-common-react => catalog-react}/src/components/index.ts (100%) rename plugins/{catalog-common-react => catalog-react}/src/hooks/index.ts (100%) rename plugins/{catalog-common-react => catalog-react}/src/hooks/useEntity.ts (100%) rename plugins/{catalog-common-react => catalog-react}/src/hooks/useEntityCompoundName.ts (100%) rename plugins/{catalog-common-react => catalog-react}/src/index.ts (100%) rename plugins/{catalog-common-react => catalog-react}/src/routes.ts (100%) rename plugins/{catalog-common-react => catalog-react}/src/setupTests.ts (100%) diff --git a/.changeset/wet-suits-live.md b/.changeset/wet-suits-live.md index c10a226367..64fcd58d97 100644 --- a/.changeset/wet-suits-live.md +++ b/.changeset/wet-suits-live.md @@ -16,4 +16,4 @@ '@backstage/plugin-techdocs': patch --- -Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-common-react`. +Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. diff --git a/.changeset/wild-cows-exercise.md b/.changeset/wild-cows-exercise.md index 411d23daee..cbc347888f 100644 --- a/.changeset/wild-cows-exercise.md +++ b/.changeset/wild-cows-exercise.md @@ -4,5 +4,5 @@ --- `@backstage/plugin-catalog` stopped exporting hooks and helpers for other -plugins. They are migrated to `@backstage/plugin-catalog-common-react`. +plugins. They are migrated to `@backstage/plugin-catalog-react`. Change both your dependencies and imports to the new package. diff --git a/packages/app/package.json b/packages/app/package.json index 72ca698f84..3fb89d96c6 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -9,7 +9,7 @@ "@backstage/core": "^0.5.0", "@backstage/plugin-api-docs": "^0.4.3", "@backstage/plugin-catalog": "^0.2.14", - "@backstage/plugin-catalog-common-react": "^0.0.1", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/plugin-catalog-import": "^0.3.6", "@backstage/plugin-circleci": "^0.2.6", "@backstage/plugin-cloudbuild": "^0.2.7", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 756d36391f..0372c8759c 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -28,7 +28,7 @@ import { ProvidingComponentsCard, } from '@backstage/plugin-api-docs'; import { AboutCard, EntityPageLayout } from '@backstage/plugin-catalog'; -import { useEntity } from '@backstage/plugin-catalog-common-react'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { isPluginApplicableToEntity as isCircleCIAvailable, Router as CircleCIRouter, diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index 4723a4b78b..e6a047921c 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -42,7 +42,7 @@ import { version as pluginApiDocs } from '../../../../plugins/api-docs/package.j import { version as pluginAppBackend } from '../../../../plugins/app-backend/package.json'; import { version as pluginAuthBackend } from '../../../../plugins/auth-backend/package.json'; import { version as pluginCatalog } from '../../../../plugins/catalog/package.json'; -import { version as pluginCatalogCommonReact } from '../../../../plugins/catalog-common-react/package.json'; +import { version as pluginCatalogReact } from '../../../../plugins/catalog-react/package.json'; import { version as pluginCatalogBackend } from '../../../../plugins/catalog-backend/package.json'; import { version as pluginCatalogImport } from '../../../../plugins/catalog-import/package.json'; import { version as pluginCircleci } from '../../../../plugins/circleci/package.json'; @@ -69,7 +69,7 @@ export const packageVersions = { '@backstage/plugin-app-backend': pluginAppBackend, '@backstage/plugin-auth-backend': pluginAuthBackend, '@backstage/plugin-catalog': pluginCatalog, - '@backstage/plugin-catalog-common-react': pluginCatalogCommonReact, + '@backstage/plugin-catalog-react': pluginCatalogReact, '@backstage/plugin-catalog-backend': pluginCatalogBackend, '@backstage/plugin-catalog-import': pluginCatalogImport, '@backstage/plugin-circleci': pluginCircleci, diff --git a/packages/create-app/templates/default-app/packages/app/package.json.hbs b/packages/create-app/templates/default-app/packages/app/package.json.hbs index 99e613bc40..c7561fe091 100644 --- a/packages/create-app/templates/default-app/packages/app/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/app/package.json.hbs @@ -10,7 +10,7 @@ "@backstage/core": "^{{version '@backstage/core'}}", "@backstage/plugin-api-docs": "^{{version '@backstage/plugin-api-docs'}}", "@backstage/plugin-catalog": "^{{version '@backstage/plugin-catalog'}}", - "@backstage/plugin-catalog-common-react": "^{{version '@backstage/plugin-catalog-common-react'}}", + "@backstage/plugin-catalog-react": "^{{version '@backstage/plugin-catalog-react'}}", "@backstage/plugin-catalog-import": "^{{version '@backstage/plugin-catalog-import'}}", "@backstage/plugin-scaffolder": "^{{version '@backstage/plugin-scaffolder'}}", "@backstage/plugin-techdocs": "^{{version '@backstage/plugin-techdocs'}}", diff --git a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx index 409c7919db..a3a3715ab0 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx @@ -27,7 +27,7 @@ import { } from '@backstage/plugin-catalog'; import { useEntity -} from '@backstage/plugin-catalog-common-react'; +} from '@backstage/plugin-catalog-react'; import { isPluginApplicableToEntity as isCircleCIAvailable, Router as CircleCIRouter } from '@backstage/plugin-circleci'; diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index bdc292f71e..4bd7bd1727 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -32,7 +32,7 @@ "@asyncapi/react-component": "^0.18.2", "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-common-react": "^0.0.1", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-icons/font": "^1.0.2", "@material-ui/core": "^4.11.0", diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx index e5e72af9cc..67462b630c 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx @@ -16,10 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry, storageApiRef } from '@backstage/core'; -import { - CatalogApi, - catalogApiRef, -} from '@backstage/plugin-catalog-common-react'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import React from 'react'; diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx index 3779f6a9de..df404ce7cd 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx @@ -15,7 +15,7 @@ */ import { Content, ContentHeader, SupportButton, useApi } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Button } from '@material-ui/core'; import React from 'react'; import { Link as RouterLink } from 'react-router-dom'; diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx index 870528b91d..d406e160fc 100644 --- a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx @@ -16,10 +16,7 @@ import { Entity, RELATION_CONSUMES_API } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry } from '@backstage/core'; -import { - CatalogApi, - catalogApiRef, -} from '@backstage/plugin-catalog-common-react'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx index a149fcde88..7b6ce6e18a 100644 --- a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx @@ -16,10 +16,7 @@ import { Entity, RELATION_PROVIDES_API } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry } from '@backstage/core'; -import { - CatalogApi, - catalogApiRef, -} from '@backstage/plugin-catalog-common-react'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx index 428f4d91fe..99a8c0dc28 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx @@ -16,10 +16,7 @@ import { Entity, RELATION_API_CONSUMED_BY } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry } from '@backstage/core'; -import { - CatalogApi, - catalogApiRef, -} from '@backstage/plugin-catalog-common-react'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx index 3f4b7a545b..a3341ad8d0 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx @@ -16,10 +16,7 @@ import { Entity, RELATION_API_PROVIDED_BY } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry } from '@backstage/core'; -import { - CatalogApi, - catalogApiRef, -} from '@backstage/plugin-catalog-common-react'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; diff --git a/plugins/api-docs/src/components/EntityLink/EntityLink.tsx b/plugins/api-docs/src/components/EntityLink/EntityLink.tsx index db0437ed83..dba6bb0062 100644 --- a/plugins/api-docs/src/components/EntityLink/EntityLink.tsx +++ b/plugins/api-docs/src/components/EntityLink/EntityLink.tsx @@ -18,7 +18,7 @@ import { Entity } from '@backstage/catalog-model'; import { entityRoute, entityRouteParams, -} from '@backstage/plugin-catalog-common-react'; +} from '@backstage/plugin-catalog-react'; import { Link } from '@material-ui/core'; import React, { PropsWithChildren } from 'react'; import { generatePath, Link as RouterLink } from 'react-router-dom'; diff --git a/plugins/api-docs/src/components/useRelatedEntities.ts b/plugins/api-docs/src/components/useRelatedEntities.ts index 4c161e8025..5c01d38189 100644 --- a/plugins/api-docs/src/components/useRelatedEntities.ts +++ b/plugins/api-docs/src/components/useRelatedEntities.ts @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; import { useApi } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { useAsyncRetry } from 'react-use'; // TODO: Maybe this hook is interesting for others too? diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 6deb066705..4c2017cda7 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -33,7 +33,7 @@ "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", "@backstage/integration": "^0.3.1", - "@backstage/plugin-catalog-common-react": "^0.0.1", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx b/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx index ff8d28e654..9fb170c85c 100644 --- a/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx +++ b/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx @@ -24,7 +24,7 @@ import { import { entityRoute, entityRouteParams, -} from '@backstage/plugin-catalog-common-react'; +} from '@backstage/plugin-catalog-react'; import { Button, CircularProgress, diff --git a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx index d86db71f92..344dd9d470 100644 --- a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx +++ b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx @@ -19,10 +19,7 @@ import { DiscoveryApi, errorApiRef, } from '@backstage/core'; -import { - catalogApiRef, - CatalogClient, -} from '@backstage/plugin-catalog-common-react'; +import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import React from 'react'; diff --git a/plugins/catalog-import/src/components/ImportComponentForm.tsx b/plugins/catalog-import/src/components/ImportComponentForm.tsx index 25bea7fbe3..a3d355b33f 100644 --- a/plugins/catalog-import/src/components/ImportComponentForm.tsx +++ b/plugins/catalog-import/src/components/ImportComponentForm.tsx @@ -15,7 +15,7 @@ */ import { errorApiRef, useApi } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { BackstageTheme } from '@backstage/theme'; import { Button, diff --git a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx index 9d7dd1eae1..ad9abbe797 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx @@ -19,10 +19,7 @@ import { configApiRef, errorApiRef, } from '@backstage/core'; -import { - catalogApiRef, - CatalogClient, -} from '@backstage/plugin-catalog-common-react'; +import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog-react'; import { msw, renderInTestApp } from '@backstage/test-utils'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import { rest } from 'msw'; diff --git a/plugins/catalog-common-react/.eslintrc.js b/plugins/catalog-react/.eslintrc.js similarity index 100% rename from plugins/catalog-common-react/.eslintrc.js rename to plugins/catalog-react/.eslintrc.js diff --git a/plugins/catalog-common-react/README.md b/plugins/catalog-react/README.md similarity index 100% rename from plugins/catalog-common-react/README.md rename to plugins/catalog-react/README.md diff --git a/plugins/catalog-common-react/package.json b/plugins/catalog-react/package.json similarity index 96% rename from plugins/catalog-common-react/package.json rename to plugins/catalog-react/package.json index 11b8d3b1f4..fc5f7a9d55 100644 --- a/plugins/catalog-common-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,5 +1,5 @@ { - "name": "@backstage/plugin-catalog-common-react", + "name": "@backstage/plugin-catalog-react", "version": "0.0.1", "main": "src/index.ts", "types": "src/index.ts", diff --git a/plugins/catalog-common-react/src/api.ts b/plugins/catalog-react/src/api.ts similarity index 100% rename from plugins/catalog-common-react/src/api.ts rename to plugins/catalog-react/src/api.ts diff --git a/plugins/catalog-common-react/src/components/EntityRefLink/EntityRefLink.test.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.test.tsx similarity index 100% rename from plugins/catalog-common-react/src/components/EntityRefLink/EntityRefLink.test.tsx rename to plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.test.tsx diff --git a/plugins/catalog-common-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx similarity index 100% rename from plugins/catalog-common-react/src/components/EntityRefLink/EntityRefLink.tsx rename to plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx diff --git a/plugins/catalog-common-react/src/components/EntityRefLink/EntityRefLinks.test.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.test.tsx similarity index 100% rename from plugins/catalog-common-react/src/components/EntityRefLink/EntityRefLinks.test.tsx rename to plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.test.tsx diff --git a/plugins/catalog-common-react/src/components/EntityRefLink/EntityRefLinks.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.tsx similarity index 100% rename from plugins/catalog-common-react/src/components/EntityRefLink/EntityRefLinks.tsx rename to plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.tsx diff --git a/plugins/catalog-common-react/src/components/EntityRefLink/format.test.ts b/plugins/catalog-react/src/components/EntityRefLink/format.test.ts similarity index 100% rename from plugins/catalog-common-react/src/components/EntityRefLink/format.test.ts rename to plugins/catalog-react/src/components/EntityRefLink/format.test.ts diff --git a/plugins/catalog-common-react/src/components/EntityRefLink/format.ts b/plugins/catalog-react/src/components/EntityRefLink/format.ts similarity index 100% rename from plugins/catalog-common-react/src/components/EntityRefLink/format.ts rename to plugins/catalog-react/src/components/EntityRefLink/format.ts diff --git a/plugins/catalog-common-react/src/components/EntityRefLink/index.ts b/plugins/catalog-react/src/components/EntityRefLink/index.ts similarity index 100% rename from plugins/catalog-common-react/src/components/EntityRefLink/index.ts rename to plugins/catalog-react/src/components/EntityRefLink/index.ts diff --git a/plugins/catalog-common-react/src/components/index.ts b/plugins/catalog-react/src/components/index.ts similarity index 100% rename from plugins/catalog-common-react/src/components/index.ts rename to plugins/catalog-react/src/components/index.ts diff --git a/plugins/catalog-common-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts similarity index 100% rename from plugins/catalog-common-react/src/hooks/index.ts rename to plugins/catalog-react/src/hooks/index.ts diff --git a/plugins/catalog-common-react/src/hooks/useEntity.ts b/plugins/catalog-react/src/hooks/useEntity.ts similarity index 100% rename from plugins/catalog-common-react/src/hooks/useEntity.ts rename to plugins/catalog-react/src/hooks/useEntity.ts diff --git a/plugins/catalog-common-react/src/hooks/useEntityCompoundName.ts b/plugins/catalog-react/src/hooks/useEntityCompoundName.ts similarity index 100% rename from plugins/catalog-common-react/src/hooks/useEntityCompoundName.ts rename to plugins/catalog-react/src/hooks/useEntityCompoundName.ts diff --git a/plugins/catalog-common-react/src/index.ts b/plugins/catalog-react/src/index.ts similarity index 100% rename from plugins/catalog-common-react/src/index.ts rename to plugins/catalog-react/src/index.ts diff --git a/plugins/catalog-common-react/src/routes.ts b/plugins/catalog-react/src/routes.ts similarity index 100% rename from plugins/catalog-common-react/src/routes.ts rename to plugins/catalog-react/src/routes.ts diff --git a/plugins/catalog-common-react/src/setupTests.ts b/plugins/catalog-react/src/setupTests.ts similarity index 100% rename from plugins/catalog-common-react/src/setupTests.ts rename to plugins/catalog-react/src/setupTests.ts diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 0f250d5a41..6752bcdfe6 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -33,7 +33,7 @@ "@backstage/catalog-client": "^0.3.5", "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-common-react": "^0.0.1", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/plugin-scaffolder": "^0.4.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", diff --git a/plugins/catalog/src/components/AboutCard/AboutContent.tsx b/plugins/catalog/src/components/AboutCard/AboutContent.tsx index 22dfc3c2a3..bb55e00e93 100644 --- a/plugins/catalog/src/components/AboutCard/AboutContent.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutContent.tsx @@ -19,7 +19,7 @@ import { RELATION_OWNED_BY, RELATION_PART_OF, } from '@backstage/catalog-model'; -import { EntityRefLinks } from '@backstage/plugin-catalog-common-react'; +import { EntityRefLinks } from '@backstage/plugin-catalog-react'; import { Chip, Grid, makeStyles, Typography } from '@material-ui/core'; import React from 'react'; import { getEntityRelations } from '../getEntityRelations'; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index de280e7e1b..0384c75c96 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -22,7 +22,7 @@ import { SupportButton, useApi, } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder'; import { Button, makeStyles } from '@material-ui/core'; import SettingsIcon from '@material-ui/icons/Settings'; diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 52d0ab9178..ed0becf270 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -24,7 +24,7 @@ import { EntityRefLink, EntityRefLinks, formatEntityRefTitle, -} from '@backstage/plugin-catalog-common-react'; +} from '@backstage/plugin-catalog-react'; import { Chip } from '@material-ui/core'; import Edit from '@material-ui/icons/Edit'; import OpenInNew from '@material-ui/icons/OpenInNew'; diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx index ba52c9a114..0d8ae56262 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx @@ -21,10 +21,7 @@ import { ApiProvider, ApiRegistry, } from '@backstage/core'; -import { - catalogApiRef, - EntityContext, -} from '@backstage/plugin-catalog-common-react'; +import { catalogApiRef, EntityContext } from '@backstage/plugin-catalog-react'; import { renderInTestApp, withLogCollector } from '@backstage/test-utils'; import { fireEvent } from '@testing-library/react'; import React from 'react'; diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index 24efdebb96..4f7cb09cb6 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -26,7 +26,7 @@ import { import { EntityContext, useEntityCompoundName, -} from '@backstage/plugin-catalog-common-react'; +} from '@backstage/plugin-catalog-react'; import { Box } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; import React, { diff --git a/plugins/catalog/src/components/EntityLoaderProvider/EntityLoaderProvider.tsx b/plugins/catalog/src/components/EntityLoaderProvider/EntityLoaderProvider.tsx index 555595e818..97a4263e0d 100644 --- a/plugins/catalog/src/components/EntityLoaderProvider/EntityLoaderProvider.tsx +++ b/plugins/catalog/src/components/EntityLoaderProvider/EntityLoaderProvider.tsx @@ -16,7 +16,7 @@ import { EntityContext, useEntityFromUrl, -} from '@backstage/plugin-catalog-common-react'; +} from '@backstage/plugin-catalog-react'; import React, { ReactNode } from 'react'; export const EntityLoaderProvider = ({ children }: { children: ReactNode }) => { diff --git a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx index cb4f9bc644..1f42cc7cc1 100644 --- a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx +++ b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx @@ -22,7 +22,7 @@ import { useNavigate } from 'react-router'; import { EntityContext, useEntityCompoundName, -} from '@backstage/plugin-catalog-common-react'; +} from '@backstage/plugin-catalog-react'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog'; diff --git a/plugins/catalog/src/components/EntityProvider/EntityProvider.tsx b/plugins/catalog/src/components/EntityProvider/EntityProvider.tsx index 836e2aa752..e17252e0a0 100644 --- a/plugins/catalog/src/components/EntityProvider/EntityProvider.tsx +++ b/plugins/catalog/src/components/EntityProvider/EntityProvider.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; import React, { ReactNode } from 'react'; -import { EntityContext } from '@backstage/plugin-catalog-common-react'; +import { EntityContext } from '@backstage/plugin-catalog-react'; type EntityProviderProps = { entity: Entity; diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx index 4be4713075..20b9aaba4f 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { EntityContext } from '@backstage/plugin-catalog-common-react'; +import { EntityContext } from '@backstage/plugin-catalog-react'; import { render } from '@testing-library/react'; import React from 'react'; import { isKind } from './conditions'; diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx index 4312fba5db..d5593edcc2 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { useEntity } from '@backstage/plugin-catalog-common-react'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { Children, Fragment, diff --git a/plugins/catalog/src/components/Router.tsx b/plugins/catalog/src/components/Router.tsx index b54dfef01c..3e51b74dc2 100644 --- a/plugins/catalog/src/components/Router.tsx +++ b/plugins/catalog/src/components/Router.tsx @@ -19,7 +19,7 @@ import { entityRoute, rootRoute, useEntity, -} from '@backstage/plugin-catalog-common-react'; +} from '@backstage/plugin-catalog-react'; import { Link, Typography } from '@material-ui/core'; import React, { ComponentType } from 'react'; import { Navigate, Route, Routes, useParams } from 'react-router'; diff --git a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx b/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx index 5e4212ee1b..06ea1eee27 100644 --- a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx +++ b/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx @@ -19,7 +19,7 @@ import { alertApiRef, configApiRef, Progress, useApi } from '@backstage/core'; import { catalogApiRef, formatEntityRefTitle, -} from '@backstage/plugin-catalog-common-react'; +} from '@backstage/plugin-catalog-react'; import { Button, Dialog, diff --git a/plugins/catalog/src/components/useOwnUser.ts b/plugins/catalog/src/components/useOwnUser.ts index 43d658e8d0..29d8a0d11f 100644 --- a/plugins/catalog/src/components/useOwnUser.ts +++ b/plugins/catalog/src/components/useOwnUser.ts @@ -16,7 +16,7 @@ import { UserEntity } from '@backstage/catalog-model'; import { identityApiRef, useApi } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { useAsync } from 'react-use'; import { AsyncState } from 'react-use/lib/useAsync'; diff --git a/plugins/catalog/src/extensions.tsx b/plugins/catalog/src/extensions.tsx index 2a214dde23..1eaa30b769 100644 --- a/plugins/catalog/src/extensions.tsx +++ b/plugins/catalog/src/extensions.tsx @@ -18,7 +18,7 @@ import { createRoutableExtension } from '@backstage/core'; import { catalogRouteRef, entityRouteRef, -} from '@backstage/plugin-catalog-common-react'; +} from '@backstage/plugin-catalog-react'; import { plugin } from './plugin'; export const CatalogIndexPage = plugin.provide( diff --git a/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx b/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx index 2d954fb2d2..69ae9b83d6 100644 --- a/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx +++ b/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx @@ -16,7 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { useApi } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useAsyncFn } from 'react-use'; import { filterGroupsContext, FilterGroupsContext } from './context'; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index be57a2e9f9..69a1285ab4 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -24,7 +24,7 @@ import { catalogApiRef, catalogRouteRef, entityRouteRef, -} from '@backstage/plugin-catalog-common-react'; +} from '@backstage/plugin-catalog-react'; export const plugin = createPlugin({ id: 'catalog', diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index b731146e40..f128540784 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -33,7 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-common-react": "^0.0.1", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/circleci/src/state/useBuilds.ts b/plugins/circleci/src/state/useBuilds.ts index b7143f1cb5..7968379ee4 100644 --- a/plugins/circleci/src/state/useBuilds.ts +++ b/plugins/circleci/src/state/useBuilds.ts @@ -15,7 +15,7 @@ */ import { errorApiRef, useApi } from '@backstage/core'; -import { useEntity } from '@backstage/plugin-catalog-common-react'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { BuildSummary, GitType } from 'circleci-api'; import { getOr } from 'lodash/fp'; import { useCallback, useEffect, useState } from 'react'; diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 75dec65ee2..7456d4be53 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -33,7 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-common-react": "^0.0.1", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/jenkins/src/components/useProjectSlugFromEntity.ts b/plugins/jenkins/src/components/useProjectSlugFromEntity.ts index 2c5f138402..ac2d6c7e8f 100644 --- a/plugins/jenkins/src/components/useProjectSlugFromEntity.ts +++ b/plugins/jenkins/src/components/useProjectSlugFromEntity.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useEntity } from '@backstage/plugin-catalog-common-react'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { JENKINS_ANNOTATION } from '../constants'; export const useProjectSlugFromEntity = () => { diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 03f602c049..546d20bc67 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -22,7 +22,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-common-react": "^0.0.1", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx index 595905d7a3..a28d1af15d 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { Entity } from '@backstage/catalog-model'; -import { EntityContext } from '@backstage/plugin-catalog-common-react'; +import { EntityContext } from '@backstage/plugin-catalog-react'; import { renderHook } from '@testing-library/react-hooks'; import React, { PropsWithChildren } from 'react'; import { useConsumerGroupsForEntity } from './useConsumerGroupsForEntity'; diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts index c81573e2ff..11155dca10 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { useEntity } from '@backstage/plugin-catalog-common-react'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { useMemo } from 'react'; import { KAFKA_CONSUMER_GROUP_ANNOTATION } from '../../constants'; diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx index 0d9846dba5..9609654a90 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; -import { EntityContext } from '@backstage/plugin-catalog-common-react'; +import { EntityContext } from '@backstage/plugin-catalog-react'; import { renderHook } from '@testing-library/react-hooks'; import { when } from 'jest-when'; import React, { PropsWithChildren } from 'react'; diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index c1df09ac1d..5b45bfa48b 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -34,7 +34,7 @@ "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-common-react": "^0.0.1", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx index cd4f1f9e5f..483d630db8 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx @@ -16,7 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; -import { EntityContext } from '@backstage/plugin-catalog-common-react'; +import { EntityContext } from '@backstage/plugin-catalog-react'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; import { render } from '@testing-library/react'; diff --git a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx index c9405a25c3..1322736321 100644 --- a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx +++ b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { EntityContext } from '@backstage/plugin-catalog-common-react'; +import { EntityContext } from '@backstage/plugin-catalog-react'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; import { render } from '@testing-library/react'; diff --git a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx index aa9c9c3557..e22a64bdc1 100644 --- a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx +++ b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; -import { EntityContext } from '@backstage/plugin-catalog-common-react'; +import { EntityContext } from '@backstage/plugin-catalog-react'; import { renderHook } from '@testing-library/react-hooks'; import React, { PropsWithChildren } from 'react'; import { lighthouseApiRef, WebsiteListResponse } from '../api'; diff --git a/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts b/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts index da351a6c82..e6713cf320 100644 --- a/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts +++ b/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { errorApiRef, useApi } from '@backstage/core'; -import { useEntity } from '@backstage/plugin-catalog-common-react'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { useAsync } from 'react-use'; import { LIGHTHOUSE_WEBSITE_URL_ANNOTATION } from '../../constants'; import { lighthouseApiRef } from '../api'; diff --git a/plugins/org/package.json b/plugins/org/package.json index 7cc55dbf60..e376dea5bc 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -22,7 +22,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-common-react": "^0.0.1", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index 0238fea7f6..f5a1acd5e6 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -21,7 +21,7 @@ import { RELATION_PARENT_OF, } from '@backstage/catalog-model'; import { Avatar, InfoCard } from '@backstage/core'; -import { entityRouteParams } from '@backstage/plugin-catalog-common-react'; +import { entityRouteParams } from '@backstage/plugin-catalog-react'; import { Box, Grid, Link, Tooltip, Typography } from '@material-ui/core'; import AccountTreeIcon from '@material-ui/icons/AccountTree'; import EmailIcon from '@material-ui/icons/Email'; diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx index 59260f93fc..6815fd0e55 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx @@ -16,10 +16,7 @@ import { Entity, GroupEntity } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry } from '@backstage/core'; -import { - CatalogApi, - catalogApiRef, -} from '@backstage/plugin-catalog-common-react'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; import React from 'react'; import { MembersListCard } from './MembersListCard'; diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx index cd001d6e96..f644e7db7e 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -23,7 +23,7 @@ import { Avatar, InfoCard, Progress, useApi } from '@backstage/core'; import { catalogApiRef, entityRouteParams, -} from '@backstage/plugin-catalog-common-react'; +} from '@backstage/plugin-catalog-react'; import { Box, createStyles, diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx index 3d459d7cb4..4f59c4be69 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { InfoCard, useApi, Progress } from '@backstage/core'; import { Entity } from '@backstage/catalog-model'; -import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { useAsync } from 'react-use'; import Alert from '@material-ui/lab/Alert'; import { diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx index f1897efed1..bdd0cd15f6 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -19,7 +19,7 @@ import { UserEntity, } from '@backstage/catalog-model'; import { Avatar, InfoCard } from '@backstage/core'; -import { entityRouteParams } from '@backstage/plugin-catalog-common-react'; +import { entityRouteParams } from '@backstage/plugin-catalog-react'; import { Box, Grid, Link, Tooltip, Typography } from '@material-ui/core'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index e12a27529f..39c431b3cd 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -32,7 +32,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-common-react": "^0.0.1", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx index c2d52f25d9..e9da8ef84d 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx @@ -20,7 +20,7 @@ import { createRouteRef, errorApiRef, } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; import { render, screen } from '@testing-library/react'; diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx index 651af14723..8dbf712559 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx @@ -26,7 +26,7 @@ import { SupportButton, useApi, } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Grid, makeStyles } from '@material-ui/core'; import React, { useState } from 'react'; import { useMountedState } from 'react-use'; diff --git a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx index aef07c8172..87248cb6db 100644 --- a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx +++ b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx @@ -19,7 +19,7 @@ import { RouteRef, StructuredMetadataTable } from '@backstage/core'; import { entityRoute, entityRouteParams, -} from '@backstage/plugin-catalog-common-react'; +} from '@backstage/plugin-catalog-react'; import { Button, Dialog, diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index fbb5994dcd..7ba72dad0d 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -33,7 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-common-react": "^0.0.1", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/rollbar/src/components/RollbarHome/RollbarHome.test.tsx b/plugins/rollbar/src/components/RollbarHome/RollbarHome.test.tsx index cb42be7e32..5bd885c488 100644 --- a/plugins/rollbar/src/components/RollbarHome/RollbarHome.test.tsx +++ b/plugins/rollbar/src/components/RollbarHome/RollbarHome.test.tsx @@ -21,10 +21,7 @@ import { ConfigApi, configApiRef, } from '@backstage/core'; -import { - catalogApiRef, - CatalogApi, -} from '@backstage/plugin-catalog-common-react'; +import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog-react'; import { wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi'; diff --git a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx index b6e6126b27..6aeee782c4 100644 --- a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx +++ b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx @@ -20,10 +20,7 @@ import { ConfigApi, configApiRef, } from '@backstage/core'; -import { - CatalogApi, - catalogApiRef, -} from '@backstage/plugin-catalog-common-react'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import * as React from 'react'; diff --git a/plugins/rollbar/src/hooks/useCatalogEntity.ts b/plugins/rollbar/src/hooks/useCatalogEntity.ts index 3d539f19cc..e39a7a75f7 100644 --- a/plugins/rollbar/src/hooks/useCatalogEntity.ts +++ b/plugins/rollbar/src/hooks/useCatalogEntity.ts @@ -18,7 +18,7 @@ import { useApi } from '@backstage/core'; import { catalogApiRef, useEntityCompoundName, -} from '@backstage/plugin-catalog-common-react'; +} from '@backstage/plugin-catalog-react'; import { useAsync } from 'react-use'; export function useCatalogEntity() { diff --git a/plugins/rollbar/src/hooks/useRollbarEntities.ts b/plugins/rollbar/src/hooks/useRollbarEntities.ts index 24e2d100bb..3f3415b176 100644 --- a/plugins/rollbar/src/hooks/useRollbarEntities.ts +++ b/plugins/rollbar/src/hooks/useRollbarEntities.ts @@ -15,7 +15,7 @@ */ import { configApiRef, useApi } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { useAsync } from 'react-use'; import { ROLLBAR_ANNOTATION } from '../constants'; diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index ded0544d3a..d174991bb7 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -32,7 +32,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-common-react": "^0.0.1", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 6178877330..75ee2d07df 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -27,7 +27,7 @@ import { useApi, WarningPanel, } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Button, Grid, Link, Typography } from '@material-ui/core'; import React, { useEffect } from 'react'; import { Link as RouterLink } from 'react-router-dom'; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx index 793895e699..f3f97be877 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx @@ -14,10 +14,7 @@ * limitations under the License. */ import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; -import { - CatalogApi, - catalogApiRef, -} from '@backstage/plugin-catalog-common-react'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp, renderWithEffects } from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 360abd441c..64351c7ec8 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -27,7 +27,7 @@ import { catalogApiRef, entityRoute, entityRouteParams, -} from '@backstage/plugin-catalog-common-react'; +} from '@backstage/plugin-catalog-react'; import { LinearProgress } from '@material-ui/core'; import { IChangeEvent } from '@rjsf/core'; import parseGitUrl from 'git-url-parse'; diff --git a/plugins/search/package.json b/plugins/search/package.json index 1185646d67..28627b88ea 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -31,7 +31,7 @@ "dependencies": { "@backstage/core": "^0.5.0", "@backstage/catalog-model": "^0.7.0", - "@backstage/plugin-catalog-common-react": "^0.0.1", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/search/src/apis.ts b/plugins/search/src/apis.ts index 34793777a8..9d88227a44 100644 --- a/plugins/search/src/apis.ts +++ b/plugins/search/src/apis.ts @@ -15,7 +15,7 @@ */ import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; -import { CatalogApi } from '@backstage/plugin-catalog-common-react'; +import { CatalogApi } from '@backstage/plugin-catalog-react'; export type Result = { name: string; diff --git a/plugins/search/src/components/SearchResult/SearchResult.tsx b/plugins/search/src/components/SearchResult/SearchResult.tsx index 72e592302c..3c43c1d24c 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.tsx @@ -21,7 +21,7 @@ import { TableColumn, useApi, } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Divider, Grid, makeStyles, Typography } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; import React, { useEffect, useState } from 'react'; diff --git a/plugins/sentry/dev/index.tsx b/plugins/sentry/dev/index.tsx index 98b687b4bf..87e03605e4 100644 --- a/plugins/sentry/dev/index.tsx +++ b/plugins/sentry/dev/index.tsx @@ -17,7 +17,7 @@ import { Entity } from '@backstage/catalog-model'; import { Content, Header, Page } from '@backstage/core'; import { createDevApp, EntityGridItem } from '@backstage/dev-utils'; -import { EntityProvider } from '@backstage/plugin-catalog-common-react'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; import { Grid } from '@material-ui/core'; import React from 'react'; import { diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 2d5edaabb5..38e1c55ed0 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -33,7 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-common-react": "^0.0.1", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/sentry/src/extensions.tsx b/plugins/sentry/src/extensions.tsx index b606beed37..ea63cd472a 100644 --- a/plugins/sentry/src/extensions.tsx +++ b/plugins/sentry/src/extensions.tsx @@ -18,7 +18,7 @@ import { createComponentExtension, createRoutableExtension, } from '@backstage/core'; -import { useEntity } from '@backstage/plugin-catalog-common-react'; +import { useEntity } from '@backstage/plugin-catalog-react'; import React from 'react'; import { plugin, rootRouteRef } from './plugin'; diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 5bd8ac0d9d..90c8fd3840 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -33,7 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-common-react": "^0.0.1", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/test-utils": "^0.1.6", "@backstage/theme": "^0.2.2", "@backstage/techdocs-common": "^0.3.6", diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx index bfca7aefe3..fb951f0a19 100644 --- a/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx @@ -15,10 +15,7 @@ */ import { ApiProvider, ApiRegistry } from '@backstage/core'; -import { - CatalogApi, - catalogApiRef, -} from '@backstage/plugin-catalog-common-react'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import React from 'react'; diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.tsx index 63dd0dceae..2d8b5c7dcc 100644 --- a/plugins/techdocs/src/reader/components/TechDocsHome.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsHome.tsx @@ -22,7 +22,7 @@ import { Progress, useApi, } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-common-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Grid } from '@material-ui/core'; import React from 'react'; import { generatePath, useNavigate } from 'react-router-dom'; From c7b89e15571e6c1a596c50e0d24ae929d278641b Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 26 Jan 2021 19:40:03 +0100 Subject: [PATCH 35/99] Move EntityProvider to plugin-catalog-react --- packages/dev-utils/package.json | 2 +- .../src/components/EntityGridItem/EntityGridItem.tsx | 6 +++--- .../src/components/EntityProvider/EntityProvider.tsx | 2 +- .../src/components/EntityProvider/index.ts | 0 plugins/catalog-react/src/components/index.ts | 1 + .../src/components/CatalogFilter/AllServicesCount.tsx | 2 +- .../src/components/CatalogFilter/CatalogFilter.test.tsx | 2 +- .../catalog/src/components/CatalogPage/CatalogPage.test.tsx | 2 +- .../src/components/ResultsFilter/ResultsFilter.test.tsx | 2 +- plugins/catalog/src/filter/useEntityFilterGroup.test.tsx | 2 +- plugins/catalog/src/index.ts | 1 - 11 files changed, 11 insertions(+), 11 deletions(-) rename plugins/{catalog => catalog-react}/src/components/EntityProvider/EntityProvider.tsx (93%) rename plugins/{catalog => catalog-react}/src/components/EntityProvider/index.ts (100%) diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 93a5247c82..eff2d6e218 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -31,7 +31,7 @@ "dependencies": { "@backstage/core": "^0.5.0", "@backstage/catalog-model": "^0.7.0", - "@backstage/plugin-catalog": "^0.2.14", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/test-utils": "^0.1.5", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", diff --git a/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx b/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx index 077a8f85e8..825a207064 100644 --- a/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx +++ b/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx @@ -14,11 +14,11 @@ * limitations under the License. */ -import React from 'react'; -import { Grid, GridProps, makeStyles } from '@material-ui/core'; import { Entity } from '@backstage/catalog-model'; -import { EntityProvider } from '@backstage/plugin-catalog'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; import { BackstageTheme } from '@backstage/theme'; +import { Grid, GridProps, makeStyles } from '@material-ui/core'; +import React from 'react'; const useStyles = makeStyles(theme => ({ root: ({ entity }) => ({ diff --git a/plugins/catalog/src/components/EntityProvider/EntityProvider.tsx b/plugins/catalog-react/src/components/EntityProvider/EntityProvider.tsx similarity index 93% rename from plugins/catalog/src/components/EntityProvider/EntityProvider.tsx rename to plugins/catalog-react/src/components/EntityProvider/EntityProvider.tsx index e17252e0a0..45cdd56d04 100644 --- a/plugins/catalog/src/components/EntityProvider/EntityProvider.tsx +++ b/plugins/catalog-react/src/components/EntityProvider/EntityProvider.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; import React, { ReactNode } from 'react'; -import { EntityContext } from '@backstage/plugin-catalog-react'; +import { EntityContext } from '../../hooks'; type EntityProviderProps = { entity: Entity; diff --git a/plugins/catalog/src/components/EntityProvider/index.ts b/plugins/catalog-react/src/components/EntityProvider/index.ts similarity index 100% rename from plugins/catalog/src/components/EntityProvider/index.ts rename to plugins/catalog-react/src/components/EntityProvider/index.ts diff --git a/plugins/catalog-react/src/components/index.ts b/plugins/catalog-react/src/components/index.ts index 7719984d2b..fcfa8207b3 100644 --- a/plugins/catalog-react/src/components/index.ts +++ b/plugins/catalog-react/src/components/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export * from './EntityRefLink'; +export * from './EntityProvider'; diff --git a/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx b/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx index e78cedbe2d..efacfa4320 100644 --- a/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx +++ b/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx @@ -15,10 +15,10 @@ */ import { useApi } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { CircularProgress, useTheme } from '@material-ui/core'; import React from 'react'; import { useAsync } from 'react-use'; -import { catalogApiRef } from '../../plugin'; export const AllServicesCount = () => { const theme = useTheme(); diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx index b92fc3d4bc..0cc2108985 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx @@ -23,11 +23,11 @@ import { identityApiRef, storageApiRef, } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; import { fireEvent, render, waitFor } from '@testing-library/react'; import React from 'react'; import { EntityFilterGroupsProvider } from '../../filter'; -import { catalogApiRef } from '../../plugin'; import { ButtonGroup, CatalogFilter } from './CatalogFilter'; describe('Catalog Filter', () => { diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 28fb1b08c7..42c9cf1117 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -28,11 +28,11 @@ import { ProfileInfo, storageApiRef, } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; import { fireEvent, render } from '@testing-library/react'; import React from 'react'; import { EntityFilterGroupsProvider } from '../../filter'; -import { catalogApiRef } from '../../plugin'; import { CatalogPage } from './CatalogPage'; describe('CatalogPage', () => { diff --git a/plugins/catalog/src/components/ResultsFilter/ResultsFilter.test.tsx b/plugins/catalog/src/components/ResultsFilter/ResultsFilter.test.tsx index d105979600..9be1c995e8 100644 --- a/plugins/catalog/src/components/ResultsFilter/ResultsFilter.test.tsx +++ b/plugins/catalog/src/components/ResultsFilter/ResultsFilter.test.tsx @@ -23,11 +23,11 @@ import { identityApiRef, storageApiRef, } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import React from 'react'; import { EntityFilterGroupsProvider } from '../../filter'; -import { catalogApiRef } from '../../plugin'; import { ResultsFilter } from './ResultsFilter'; describe('Results Filter', () => { diff --git a/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx b/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx index e9af063160..685fc751d2 100644 --- a/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx +++ b/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx @@ -15,10 +15,10 @@ */ import { ApiProvider, ApiRegistry, storageApiRef } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { MockStorageApi } from '@backstage/test-utils'; import { act, renderHook } from '@testing-library/react-hooks'; import React from 'react'; -import { catalogApiRef } from '../plugin'; import { EntityFilterGroupsProvider } from './EntityFilterGroupsProvider'; import { FilterGroup, FilterGroupStatesReady } from './types'; import { useEntityFilterGroup } from './useEntityFilterGroup'; diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 5a93b80539..80367f80e4 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -17,7 +17,6 @@ export { AboutCard } from './components/AboutCard'; export { EntityLayout } from './components/EntityLayout'; export { EntityPageLayout } from './components/EntityPageLayout'; -export { EntityProvider } from './components/EntityProvider'; export * from './components/EntitySwitch'; export { Router } from './components/Router'; export * from './extensions'; From a243dcbcd839027a17228976714e3432c49b9081 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 27 Jan 2021 09:39:40 +0100 Subject: [PATCH 36/99] Update readme --- .changeset/wet-suits-live.md | 1 + plugins/catalog-react/README.md | 17 +++++++---------- plugins/catalog/package.json | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/.changeset/wet-suits-live.md b/.changeset/wet-suits-live.md index 64fcd58d97..27b8df8a84 100644 --- a/.changeset/wet-suits-live.md +++ b/.changeset/wet-suits-live.md @@ -14,6 +14,7 @@ '@backstage/plugin-search': patch '@backstage/plugin-sentry': patch '@backstage/plugin-techdocs': patch +'@backstage/dev-utils': patch --- Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. diff --git a/plugins/catalog-react/README.md b/plugins/catalog-react/README.md index 574e23cf62..65ab45b9b2 100644 --- a/plugins/catalog-react/README.md +++ b/plugins/catalog-react/README.md @@ -1,17 +1,14 @@ -# Catalog Client +# Catalog React -Contains a frontend and backend compatible client for communicating with the -Backstage Catalog. +WORK IN PROGRESS -Backend code may import and use this package directly. +This is shared code of the frontend part of the default catalog plugin. -However, frontend code will not want to import this package directly - use the -`@backstage/plugin-catalog` package instead, which re-exports all of the types -and classes from this package. Thereby, you will also gain access to its -`catalogApiRef`. +It will implement the core API for handling your catalog of software, and +supplies components that can be reused by third-party plugins. ## Links -- [Default frontend part of the catalog](https://github.com/spotify/backstage/tree/master/plugins/catalog) -- [Default backend part of the catalog](https://github.com/spotify/backstage/tree/master/plugins/catalog-backend) +- [Frontend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/catalog) +- [Backend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend) - [The Backstage homepage](https://backstage.io) diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 6752bcdfe6..bf30e28571 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -69,4 +69,4 @@ "files": [ "dist" ] -} \ No newline at end of file +} From 93cd587142eecd6b359b63fb3d30efa8a4baf15a Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 29 Jan 2021 10:56:23 +0100 Subject: [PATCH 37/99] Fix build --- plugins/catalog-react/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index fc5f7a9d55..3c0402a194 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -40,10 +40,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", - "@microsoft/microsoft-graph-types": "^1.25.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/react-hooks": "^3.3.0", From 5207834e78965cebc13375a260d2037d57678b7a Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 29 Jan 2021 11:17:33 +0100 Subject: [PATCH 38/99] chore: need to run yarn install on build as the base image node modules are outdated --- .tugboat/config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index 2be043cd78..09422032bc 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -10,6 +10,5 @@ services: - echo "yarn --cwd ${TUGBOAT_ROOT} start-backend --config ${TUGBOAT_ROOT}/app-config.yaml --config ${TUGBOAT_ROOT}/.tugboat/tugboat.app-config.production.yaml" >> /etc/service/node/run - chmod +x /etc/service/node/run build: - - yarn workspace example-app build - update: - yarn install + - yarn workspace example-app build From f44137454fb5e8e4285ed48d6c1735c26582e142 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 29 Jan 2021 11:20:27 +0100 Subject: [PATCH 39/99] chore: should install with frozen lockfile --- .tugboat/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index 09422032bc..594a1044ac 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -10,5 +10,5 @@ services: - echo "yarn --cwd ${TUGBOAT_ROOT} start-backend --config ${TUGBOAT_ROOT}/app-config.yaml --config ${TUGBOAT_ROOT}/.tugboat/tugboat.app-config.production.yaml" >> /etc/service/node/run - chmod +x /etc/service/node/run build: - - yarn install + - yarn install --frozen-lockfile - yarn workspace example-app build From 0cb07157006249136674ed9229f298c2ccd0eb82 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 29 Jan 2021 13:42:53 +0100 Subject: [PATCH 40/99] Scaffolder: Fix tests in windows --- .../src/scaffolder/jobs/processor.test.ts | 25 ++++++++++-------- .../scaffolder/stages/prepare/file.test.ts | 4 +-- .../scaffolder/stages/publish/azure.test.ts | 9 +++++-- .../stages/publish/bitbucket.test.ts | 13 +++++++--- .../scaffolder/stages/publish/github.test.ts | 26 ++++++++++++------- .../scaffolder/stages/publish/gitlab.test.ts | 13 +++++++--- .../stages/templater/cookiecutter.test.ts | 26 +++++++++++-------- 7 files changed, 72 insertions(+), 44 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts index 0492400db6..5ff2bd1640 100644 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import os from 'os'; import { JobProcessor } from './processor'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { StageInput } from './types'; @@ -68,9 +69,11 @@ describe('JobProcessor', () => { }, }; + const workingDirectory = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; + describe('create', () => { it('creates should create a new job with a unique id', async () => { - const processor = new JobProcessor('/tmp'); + const processor = new JobProcessor(workingDirectory); const job = processor.create({ entity: mockEntity, @@ -84,7 +87,7 @@ describe('JobProcessor', () => { }); it('should setup the correct context for the job', async () => { - const processor = new JobProcessor('/tmp'); + const processor = new JobProcessor(workingDirectory); const job = processor.create({ entity: mockEntity, @@ -97,7 +100,7 @@ describe('JobProcessor', () => { }); it('should set the status as pending', async () => { - const processor = new JobProcessor('/tmp'); + const processor = new JobProcessor(workingDirectory); const job = processor.create({ entity: mockEntity, @@ -120,7 +123,7 @@ describe('JobProcessor', () => { }, ]; - const processor = new JobProcessor('/tmp'); + const processor = new JobProcessor(workingDirectory); const job = processor.create({ entity: mockEntity, @@ -139,12 +142,12 @@ describe('JobProcessor', () => { describe('get', () => { it('return undefined for when the job does not exist', () => { - const processor = new JobProcessor('/tmp'); + const processor = new JobProcessor(workingDirectory); expect(processor.get('123')).not.toBeDefined(); }); it('should return the exact same instance of the job when one is created', async () => { - const processor = new JobProcessor('/tmp'); + const processor = new JobProcessor(workingDirectory); const job = processor.create({ entity: mockEntity, values: mockValues, @@ -156,7 +159,7 @@ describe('JobProcessor', () => { }); describe('process', () => { it('throws an error when the status of the job is not in pending state', async () => { - const processor = new JobProcessor('/tmp'); + const processor = new JobProcessor(workingDirectory); const job = processor.create({ entity: mockEntity, values: mockValues, @@ -182,7 +185,7 @@ describe('JobProcessor', () => { }, ]; - const processor = new JobProcessor('/tmp'); + const processor = new JobProcessor(workingDirectory); const job = processor.create({ entity: mockEntity, values: mockValues, @@ -208,7 +211,7 @@ describe('JobProcessor', () => { }, ]; - const processor = new JobProcessor('/tmp'); + const processor = new JobProcessor(workingDirectory); const job = processor.create({ entity: mockEntity, values: mockValues, @@ -244,7 +247,7 @@ describe('JobProcessor', () => { }, ]; - const processor = new JobProcessor('/tmp'); + const processor = new JobProcessor(workingDirectory); const job = processor.create({ entity: mockEntity, values: mockValues, @@ -283,7 +286,7 @@ describe('JobProcessor', () => { }, ]; - const processor = new JobProcessor('/tmp'); + const processor = new JobProcessor(workingDirectory); const job = processor.create({ entity: mockEntity, values: mockValues, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts index 408653aee0..ffb89b68b1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts @@ -18,7 +18,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import fs from 'fs-extra'; import { FilePreparer } from './file'; import os from 'os'; -import { resolve } from 'path'; +import path, { resolve } from 'path'; jest.mock('fs-extra'); @@ -35,7 +35,7 @@ describe('File preparer', () => { workspacePath, }); expect(fs.copy).toHaveBeenCalledWith( - resolve('/path', 'to', 'template'), + path.join('/path', 'to', 'template'), checkoutPath, { recursive: true, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts index 81901809e4..1ce47e04dd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts @@ -20,6 +20,8 @@ jest.mock('azure-devops-node-api', () => ({ getPersonalAccessTokenHandler: jest.fn().mockReturnValue(() => {}), })); +import os from 'os'; +import { resolve } from 'path'; import { AzurePublisher } from './azure'; import { WebApi } from 'azure-devops-node-api'; import * as helpers from './helpers'; @@ -28,6 +30,9 @@ import { getVoidLogger } from '@backstage/backend-common'; describe('Azure Publisher', () => { const logger = getVoidLogger(); + const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; + const resultPath = resolve(workspacePath, 'result'); + describe('publish: createRemoteInAzure', () => { it('should use azure-devops-node-api to create a repo in the given project', async () => { const mockGitClient = { @@ -53,7 +58,7 @@ describe('Azure Publisher', () => { storePath: 'https://dev.azure.com/organisation/project/_git/repo', owner: 'bob', }, - workspacePath: '/tmp/test', + workspacePath, logger, }); @@ -74,7 +79,7 @@ describe('Azure Publisher', () => { 'project', ); expect(helpers.initRepoAndPush).toHaveBeenCalledWith({ - dir: '/tmp/test/result', + dir: resultPath, remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', auth: { username: 'notempty', password: 'fake-azure-token' }, logger, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts index 3b6ec9da89..419c81ea7e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts @@ -16,6 +16,8 @@ jest.mock('./helpers'); +import os from 'os'; +import { resolve } from 'path'; import { BitbucketPublisher } from './bitbucket'; import { initRepoAndPush } from './helpers'; import { getVoidLogger } from '@backstage/backend-common'; @@ -32,6 +34,9 @@ describe('Bitbucket Publisher', () => { jest.clearAllMocks(); }); + const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; + const resultPath = resolve(workspacePath, 'result'); + describe('publish: createRemoteInBitbucketCloud', () => { it('should create repo in bitbucket cloud', async () => { server.use( @@ -69,7 +74,7 @@ describe('Bitbucket Publisher', () => { storePath: 'https://bitbucket.org/project/repo', owner: 'bob', }, - workspacePath: '/tmp/test', + workspacePath, logger: logger, }); @@ -80,7 +85,7 @@ describe('Bitbucket Publisher', () => { }); expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: '/tmp/test/result', + dir: resultPath, remoteUrl: 'https://bitbucket.org/project/repo', auth: { username: 'fake-user', password: 'fake-token' }, logger: logger, @@ -127,7 +132,7 @@ describe('Bitbucket Publisher', () => { storePath: 'https://bitbucket.mycompany.com/project/repo', owner: 'bob', }, - workspacePath: '/tmp/test', + workspacePath, logger: logger, }); @@ -138,7 +143,7 @@ describe('Bitbucket Publisher', () => { }); expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: '/tmp/test/result', + dir: resultPath, remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', auth: { username: 'x-token-auth', password: 'fake-token' }, logger: logger, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts index faa8c49124..30fe8fd65a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts @@ -17,6 +17,9 @@ jest.mock('@octokit/rest'); jest.mock('./helpers'); +import os from 'os'; +import { resolve } from 'path'; + import { getVoidLogger } from '@backstage/backend-common'; import { Octokit, RestEndpointMethodTypes } from '@octokit/rest'; import { GithubPublisher } from './github'; @@ -36,6 +39,9 @@ describe('GitHub Publisher', () => { jest.clearAllMocks(); }); + const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; + const resultPath = resolve(workspacePath, 'result'); + describe('with public repo visibility', () => { describe('publish: createRemoteInGithub', () => { it('should use octokit to create a repo in an organisation if the organisation property is set', async () => { @@ -64,7 +70,7 @@ describe('GitHub Publisher', () => { owner: 'bob', access: 'blam/team', }, - workspacePath: '/tmp/test', + workspacePath, logger, }); @@ -89,7 +95,7 @@ describe('GitHub Publisher', () => { permission: 'admin', }); expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: '/tmp/test/result', + dir: resultPath, remoteUrl: 'https://github.com/backstage/backstage.git', auth: { username: 'fake-token', password: 'x-oauth-basic' }, logger, @@ -122,7 +128,7 @@ describe('GitHub Publisher', () => { owner: 'bob', access: 'blam', }, - workspacePath: '/tmp/test', + workspacePath, logger, }); @@ -140,7 +146,7 @@ describe('GitHub Publisher', () => { expect(mockGithubClient.repos.addCollaborator).not.toHaveBeenCalled(); expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: '/tmp/test/result', + dir: resultPath, remoteUrl: 'https://github.com/backstage/backstage.git', auth: { username: 'fake-token', password: 'x-oauth-basic' }, logger, @@ -175,7 +181,7 @@ describe('GitHub Publisher', () => { access: 'bob', description: 'description', }, - workspacePath: '/tmp/test', + workspacePath, logger, }); @@ -198,7 +204,7 @@ describe('GitHub Publisher', () => { permission: 'admin', }); expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: '/tmp/test/result', + dir: resultPath, remoteUrl: 'https://github.com/backstage/backstage.git', auth: { username: 'fake-token', password: 'x-oauth-basic' }, logger, @@ -233,7 +239,7 @@ describe('GitHub Publisher', () => { storePath: 'https://github.com/blam/test', owner: 'bob', }, - workspacePath: '/tmp/test', + workspacePath, logger, }); @@ -249,7 +255,7 @@ describe('GitHub Publisher', () => { visibility: 'internal', }); expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: '/tmp/test/result', + dir: resultPath, remoteUrl: 'https://github.com/backstage/backstage.git', auth: { username: 'fake-token', password: 'x-oauth-basic' }, logger, @@ -283,7 +289,7 @@ describe('GitHub Publisher', () => { storePath: 'https://github.com/blam/test', owner: 'bob', }, - workspacePath: '/tmp/test', + workspacePath, logger, }); @@ -299,7 +305,7 @@ describe('GitHub Publisher', () => { private: true, }); expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: '/tmp/test/result', + dir: resultPath, remoteUrl: 'https://github.com/backstage/backstage.git', auth: { username: 'fake-token', password: 'x-oauth-basic' }, logger, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts index 99ddbbb2b7..a97c6a6624 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts @@ -20,6 +20,8 @@ jest.mock('@gitbeaker/node', () => ({ jest.mock('./helpers'); +import os from 'os'; +import { resolve } from 'path'; import { GitlabPublisher } from './gitlab'; import { Gitlab } from '@gitbeaker/node'; import { initRepoAndPush } from './helpers'; @@ -47,6 +49,9 @@ describe('GitLab Publisher', () => { ); }); + const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; + const resultPath = resolve(workspacePath, 'result'); + describe('publish: createRemoteInGitLab', () => { it('should use gitbeaker to create a repo in a namespace if the namespace property is set', async () => { const publisher = await GitlabPublisher.fromConfig({ @@ -68,7 +73,7 @@ describe('GitLab Publisher', () => { storePath: 'https://gitlab.com/blam/test', owner: 'bob', }, - workspacePath: '/tmp/test', + workspacePath, logger, }); @@ -85,7 +90,7 @@ describe('GitLab Publisher', () => { name: 'test', }); expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: '/tmp/test/result', + dir: resultPath, remoteUrl: 'mockclone', auth: { username: 'oauth2', password: 'fake-token' }, logger, @@ -111,7 +116,7 @@ describe('GitLab Publisher', () => { storePath: 'https://gitlab.com/blam/test', owner: 'bob', }, - workspacePath: '/tmp/test', + workspacePath, logger, }); @@ -125,7 +130,7 @@ describe('GitLab Publisher', () => { name: 'test', }); expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: '/tmp/test/result', + dir: resultPath, remoteUrl: 'mockclone', auth: { username: 'oauth2', password: 'fake-token' }, logger, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts index f3c716e8a7..8172986d69 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts @@ -24,6 +24,7 @@ jest.mock('fs-extra'); import { CookieCutter } from './cookiecutter'; import fs from 'fs-extra'; +import path from 'path'; import { PassThrough } from 'stream'; import Docker from 'dockerode'; import parseGitUrl from 'git-url-parse'; @@ -56,7 +57,7 @@ describe('CookieCutter Templater', () => { }); expect(fs.writeJson).toBeCalledWith( - 'tempdir/template/cookiecutter.json', + path.join('tempdir', 'template', 'cookiecutter.json'), expect.objectContaining(values), ); }); @@ -87,13 +88,16 @@ describe('CookieCutter Templater', () => { dockerClient: mockDocker, }); - expect(fs.writeJSON).toBeCalledWith('tempdir/template/cookiecutter.json', { - ...existingJson, - ...values, - destination: { - git: expect.objectContaining({ organization: 'org', name: 'repo' }), + expect(fs.writeJSON).toBeCalledWith( + path.join('tempdir', 'template', 'cookiecutter.json'), + { + ...existingJson, + ...values, + destination: { + git: expect.objectContaining({ organization: 'org', name: 'repo' }), + }, }, - }); + ); }); it('should throw an error if the cookiecutter json is malformed and not missing', async () => { @@ -148,8 +152,8 @@ describe('CookieCutter Templater', () => { '/template', '--verbose', ], - templateDir: 'tempdir/template', - resultDir: 'tempdir/intermediate', + templateDir: path.join('tempdir', 'template'), + resultDir: path.join('tempdir', 'intermediate'), logStream: undefined, dockerClient: mockDocker, }); @@ -187,8 +191,8 @@ describe('CookieCutter Templater', () => { '/template', '--verbose', ], - templateDir: 'tempdir/template', - resultDir: 'tempdir/intermediate', + templateDir: path.join('tempdir', 'template'), + resultDir: path.join('tempdir', 'intermediate'), logStream: stream, dockerClient: mockDocker, }); From adaba84f3fb0ad7aadecd2b8aa4b4dc40f445e0d Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 29 Jan 2021 13:51:46 +0100 Subject: [PATCH 41/99] import path instead of resolve --- .../src/scaffolder/stages/prepare/azure.test.ts | 13 ++++++------- .../src/scaffolder/stages/prepare/bitbucket.test.ts | 10 +++++----- .../src/scaffolder/stages/prepare/file.test.ts | 4 ++-- .../src/scaffolder/stages/prepare/github.test.ts | 10 +++++----- .../src/scaffolder/stages/prepare/gitlab.test.ts | 10 +++++----- 5 files changed, 23 insertions(+), 24 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts index 9c4cccb5dc..6637a68e4b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts @@ -16,7 +16,7 @@ import fs from 'fs-extra'; import os from 'os'; -import { resolve } from 'path'; +import path from 'path'; import { AzurePreparer } from './azure'; import { getVoidLogger, Git } from '@backstage/backend-common'; @@ -37,8 +37,8 @@ describe('AzurePreparer', () => { }); const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; - const checkoutPath = resolve(workspacePath, 'checkout'); - const templatePath = resolve(workspacePath, 'template'); + const checkoutPath = path.resolve(workspacePath, 'checkout'); + const templatePath = path.resolve(workspacePath, 'template'); const prepareOptions = { url: 'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo', @@ -54,7 +54,7 @@ describe('AzurePreparer', () => { username: 'notempty', }); expect(fs.move).toHaveBeenCalledWith(checkoutPath, templatePath); - expect(fs.rmdir).toHaveBeenCalledWith(resolve(templatePath, '.git')); + expect(fs.rmdir).toHaveBeenCalledWith(path.resolve(templatePath, '.git')); }); it('calls the clone command with the correct arguments for a repository', async () => { @@ -100,17 +100,16 @@ describe('AzurePreparer', () => { }); it('moves the template from path if it is specified', async () => { - const path = './subdir'; await preparer.prepare({ url: `https://dev.azure.com/backstage-org/backstage-project/_git/template-repo?path=${encodeURIComponent( - path, + './subdir', )}`, logger, workspacePath, }); expect(fs.move).toHaveBeenCalledWith( - resolve(checkoutPath, 'subdir'), + path.resolve(checkoutPath, 'subdir'), templatePath, ); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts index cf08b888f4..beba62eb2a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts @@ -17,7 +17,7 @@ import fs from 'fs-extra'; import { BitbucketPreparer } from './bitbucket'; import { getVoidLogger, Git } from '@backstage/backend-common'; -import { resolve } from 'path'; +import path from 'path'; import os from 'os'; jest.mock('fs-extra'); @@ -41,8 +41,8 @@ describe('BitbucketPreparer', () => { }); const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; - const checkoutPath = resolve(workspacePath, 'checkout'); - const templatePath = resolve(workspacePath, 'template'); + const checkoutPath = path.resolve(workspacePath, 'checkout'); + const templatePath = path.resolve(workspacePath, 'template'); const prepareOptions = { url: 'https://bitbucket.org/backstage-project/backstage-repo', @@ -58,7 +58,7 @@ describe('BitbucketPreparer', () => { ref: expect.any(String), }); expect(fs.move).toHaveBeenCalledWith(checkoutPath, templatePath); - expect(fs.rmdir).toHaveBeenCalledWith(resolve(templatePath, '.git')); + expect(fs.rmdir).toHaveBeenCalledWith(path.resolve(templatePath, '.git')); }); it('calls the clone command with the correct arguments if an app password is provided for a repository', async () => { @@ -92,7 +92,7 @@ describe('BitbucketPreparer', () => { workspacePath, }); expect(fs.move).toHaveBeenCalledWith( - resolve(checkoutPath, '1', '2', '3'), + path.resolve(checkoutPath, '1', '2', '3'), templatePath, ); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts index ffb89b68b1..36be4d705b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts @@ -18,7 +18,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import fs from 'fs-extra'; import { FilePreparer } from './file'; import os from 'os'; -import path, { resolve } from 'path'; +import path from 'path'; jest.mock('fs-extra'); @@ -27,7 +27,7 @@ describe('File preparer', () => { const logger = getVoidLogger(); const preparer = new FilePreparer(); const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; - const checkoutPath = resolve(workspacePath, 'checkout'); + const checkoutPath = path.resolve(workspacePath, 'checkout'); await preparer.prepare({ url: 'file:///path/to/template', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index fd8ab1880e..a5fad5eaaa 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -16,7 +16,7 @@ import fs from 'fs-extra'; import os from 'os'; -import { resolve } from 'path'; +import path from 'path'; import { GithubPreparer } from './github'; import { getVoidLogger, Git } from '@backstage/backend-common'; @@ -24,8 +24,8 @@ jest.mock('fs-extra'); describe('GitHubPreparer', () => { const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; - const checkoutPath = resolve(workspacePath, 'checkout'); - const templatePath = resolve(workspacePath, 'template'); + const checkoutPath = path.resolve(workspacePath, 'checkout'); + const templatePath = path.resolve(workspacePath, 'template'); const mockGitClient = { clone: jest.fn(), @@ -57,7 +57,7 @@ describe('GitHubPreparer', () => { ref: expect.any(String), }); expect(fs.move).toHaveBeenCalledWith( - resolve(checkoutPath, 'templates', 'graphql-starter', 'template'), + path.resolve(checkoutPath, 'templates', 'graphql-starter', 'template'), templatePath, ); expect(fs.rmdir).toHaveBeenCalledWith('/tmp/template/.git'); @@ -77,7 +77,7 @@ describe('GitHubPreparer', () => { ref: 'master', }); expect(fs.move).toHaveBeenCalledWith(checkoutPath, templatePath); - expect(fs.rmdir).toHaveBeenCalledWith(resolve(templatePath, '.git')); + expect(fs.rmdir).toHaveBeenCalledWith(path.resolve(templatePath, '.git')); }); it('calls the clone command with token', async () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts index 6c04ce3a00..6ed4651e4f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -15,7 +15,7 @@ */ import fs from 'fs-extra'; import os from 'os'; -import { resolve } from 'path'; +import path from 'path'; import { GitlabPreparer } from './gitlab'; import { getVoidLogger, Git } from '@backstage/backend-common'; @@ -23,8 +23,8 @@ jest.mock('fs-extra'); describe('GitLabPreparer', () => { const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; - const checkoutPath = resolve(workspacePath, 'checkout'); - const templatePath = resolve(workspacePath, 'template'); + const checkoutPath = path.resolve(workspacePath, 'checkout'); + const templatePath = path.resolve(workspacePath, 'template'); const mockGitClient = { clone: jest.fn(), @@ -62,7 +62,7 @@ describe('GitLabPreparer', () => { }); expect(fs.move).toHaveBeenCalledWith(checkoutPath, templatePath); - expect(fs.rmdir).toHaveBeenCalledWith(resolve(templatePath, '.git')); + expect(fs.rmdir).toHaveBeenCalledWith(path.resolve(templatePath, '.git')); }); it(`clones the template from a sub directory if specified`, async () => { @@ -73,7 +73,7 @@ describe('GitLabPreparer', () => { workspacePath, }); expect(fs.move).toHaveBeenCalledWith( - resolve(checkoutPath, '1', '2', '3'), + path.resolve(checkoutPath, '1', '2', '3'), templatePath, ); }); From 262f7cf99637dd641fba8f95a5bab4ba0bba6ba6 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 29 Jan 2021 13:53:12 +0100 Subject: [PATCH 42/99] import path instead of resolve --- .../src/scaffolder/stages/publish/gitlab.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts index a97c6a6624..368f1bb29f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts @@ -21,7 +21,7 @@ jest.mock('@gitbeaker/node', () => ({ jest.mock('./helpers'); import os from 'os'; -import { resolve } from 'path'; +import path from 'path'; import { GitlabPublisher } from './gitlab'; import { Gitlab } from '@gitbeaker/node'; import { initRepoAndPush } from './helpers'; @@ -50,7 +50,7 @@ describe('GitLab Publisher', () => { }); const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; - const resultPath = resolve(workspacePath, 'result'); + const resultPath = path.resolve(workspacePath, 'result'); describe('publish: createRemoteInGitLab', () => { it('should use gitbeaker to create a repo in a namespace if the namespace property is set', async () => { From 8e461357022c21299cf89afb182d62a89974a170 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Fri, 29 Jan 2021 14:10:15 +0100 Subject: [PATCH 43/99] docs: Remove broken installation page --- microsite/sidebars.json | 1 - 1 file changed, 1 deletion(-) diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 99bd430b56..f855327627 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -13,7 +13,6 @@ "Getting Started": [ "getting-started/index", "getting-started/running-backstage-locally", - "getting-started/installation", "getting-started/development-environment", "getting-started/create-an-app", { From 26a6be27c023c786015f4cbd5f164e072cbf4044 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 29 Jan 2021 15:13:48 +0100 Subject: [PATCH 44/99] Fix publishConfig --- plugins/catalog-react/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 3c0402a194..94dc835d14 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -7,8 +7,7 @@ "private": false, "publishConfig": { "access": "public", - "main": "dist/index.cjs.js", - "module": "dist/index.esm.js", + "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, "homepage": "https://backstage.io", From 70906eed4cda0582aea7c4493494333c3afa841d Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Thu, 28 Jan 2021 18:55:36 -0500 Subject: [PATCH 45/99] refactor --- .../src/alerts/KubernetesMigrationAlert.tsx | 9 +- plugins/cost-insights/src/client.ts | 4 +- .../AlertInsights/AlertDialog.test.tsx | 170 +++++++++--------- .../components/AlertInsights/AlertDialog.tsx | 152 +++++----------- .../AlertInsights/AlertInsights.test.tsx | 12 +- .../AlertInsights/AlertInsights.tsx | 119 ++++++------ .../AlertInsights/AlertInsightsHeader.tsx | 3 +- .../AlertInsightsSection.test.tsx | 97 +++++----- .../AlertInsights/AlertInsightsSection.tsx | 26 ++- .../AlertInsightsSectionHeader.tsx | 3 +- .../AlertInsights/AlertStatusSummary.tsx | 130 +++++++------- .../CostInsightsPage/CostInsightsPage.tsx | 45 ++--- .../CostInsightsPage/CostInsightsPageRoot.tsx | 5 +- .../CostOverviewCard/CostOverviewCard.tsx | 4 +- .../ProductInsightsCard.tsx | 3 +- .../src/forms/AlertSnoozeForm.tsx | 26 +-- plugins/cost-insights/src/hooks/index.ts | 1 - plugins/cost-insights/src/hooks/useAlerts.tsx | 76 -------- plugins/cost-insights/src/hooks/useScroll.tsx | 56 +----- .../cost-insights/src/utils/alerts.test.tsx | 109 +++++++++++ plugins/cost-insights/src/utils/alerts.tsx | 124 +++++++++++-- plugins/cost-insights/src/utils/scroll.tsx | 65 +++++++ plugins/cost-insights/src/utils/tests.tsx | 27 --- 23 files changed, 646 insertions(+), 620 deletions(-) delete mode 100644 plugins/cost-insights/src/hooks/useAlerts.tsx create mode 100644 plugins/cost-insights/src/utils/alerts.test.tsx create mode 100644 plugins/cost-insights/src/utils/scroll.tsx diff --git a/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx b/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx index 8c59d0c1a5..31a4d7e0e9 100644 --- a/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx +++ b/plugins/cost-insights/src/alerts/KubernetesMigrationAlert.tsx @@ -72,7 +72,6 @@ export class KubernetesMigrationAlert implements MigrationAlert { // Dialog will not render a form if form property set to null. AcceptForm = null; - // Overrides default Dismiss form with a custom form component. DismissForm: AlertForm< MigrationAlert, @@ -94,7 +93,7 @@ export class KubernetesMigrationAlert implements MigrationAlert { get element() { const subheader = `${pluralize( - 'Compute Engine role', + 'Service', this.data.services.length, true, )}, sorted by cost`; @@ -116,7 +115,7 @@ export class KubernetesMigrationAlert implements MigrationAlert { const alerts = await this.api.getAlerts(options.group); return new Promise(resolve => setTimeout(resolve, 750, [ - ...alerts.slice(0, 2), + ...alerts.filter(a => a.title !== this.title), { title: this.title, subtitle: this.subtitle, @@ -133,7 +132,7 @@ export class KubernetesMigrationAlert implements MigrationAlert { const alerts = await this.api.getAlerts(options.group); return new Promise(resolve => setTimeout(resolve, 750, [ - ...alerts.slice(0, 2), + ...alerts.filter(a => a.title !== this.title), { title: this.title, subtitle: this.subtitle, @@ -148,7 +147,7 @@ export class KubernetesMigrationAlert implements MigrationAlert { const alerts = await this.api.getAlerts(options.group); return new Promise(resolve => setTimeout(resolve, 750, [ - ...alerts.slice(0, 2), + ...alerts.filter(a => a.title !== this.title), { title: this.title, subtitle: this.subtitle, diff --git a/plugins/cost-insights/src/client.ts b/plugins/cost-insights/src/client.ts index 7849214c03..2e70170fc1 100644 --- a/plugins/cost-insights/src/client.ts +++ b/plugins/cost-insights/src/client.ts @@ -183,8 +183,8 @@ export class ExampleCostInsightsClient implements CostInsightsApi { new ProjectGrowthAlert(projectGrowthData), new UnlabeledDataflowAlert(unlabeledDataflowData), new KubernetesMigrationAlert(this, { - startDate: today.format(DEFAULT_DATE_FORMAT), - endDate: today.add(30, 'day').format(DEFAULT_DATE_FORMAT), + startDate: today.subtract(30, 'day').format(DEFAULT_DATE_FORMAT), + endDate: today.format(DEFAULT_DATE_FORMAT), change: { ratio: 0, amount: 0, diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertDialog.test.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertDialog.test.tsx index 72ff757cee..7061f0736b 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertDialog.test.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertDialog.test.tsx @@ -14,9 +14,10 @@ * limitations under the License. */ import React from 'react'; +import { capitalize } from '@material-ui/core'; import { AlertDialog } from './AlertDialog'; import { render } from '@testing-library/react'; -import { Alert, AlertFormProps } from '../../types'; +import { Alert, AlertFormProps, AlertStatus } from '../../types'; type MockFormDataProps = AlertFormProps; @@ -40,7 +41,7 @@ const dimissableAlert: Alert = { onDismissed: jest.fn(), }; -const acceptAlert: Alert = { +const acceptableAlert: Alert = { title: 'title', subtitle: 'subtitle', onAccepted: jest.fn(), @@ -87,98 +88,87 @@ const nullSnoozeAlert: Alert = { onSnoozed: jest.fn(), SnoozeForm: null, }; + describe('', () => { describe.each` - accepted | dismissed | snoozed | action | text - ${acceptAlert} | ${null} | ${null} | ${['Accept', 'accepted']} | ${'My team can commit to making this change soon, or has already.'} - ${null} | ${dimissableAlert} | ${null} | ${['Dismiss', 'dismissed']} | ${'Reason for dismissing?'} - ${null} | ${null} | ${snoozableAlert} | ${['Snooze', 'snoozed']} | ${'For how long?'} - `( - 'Default forms', - ({ accepted, dismissed, snoozed, action: [action, actioned], text }) => { - it(`Displays a default ${action} form`, () => { - const { getByText } = render( - , - ); - expect(getByText(text)).toBeInTheDocument(); - expect(getByText(`${action} this action item?`)).toBeInTheDocument(); - expect( - getByText(`This action item will be ${actioned} for all of Ramones.`), - ).toBeInTheDocument(); - }); - }, - ); + alert | status | action | text + ${acceptableAlert} | ${AlertStatus.Accepted} | ${['accept', 'accepted']} | ${'My team can commit to making this change soon, or has already.'} + ${dimissableAlert} | ${AlertStatus.Dismissed} | ${['dismiss', 'dismissed']} | ${'Reason for dismissing?'} + ${snoozableAlert} | ${AlertStatus.Snoozed} | ${['snooze', 'snoozed']} | ${'For how long?'} + `('Default forms', ({ alert, status, action: [action, actioned], text }) => { + it(`Displays a default ${action} form`, () => { + const { getByText } = render( + , + ); + expect(getByText(text)).toBeInTheDocument(); + expect( + getByText(`${capitalize(action)} this action item?`), + ).toBeInTheDocument(); + expect( + getByText(`This action item will be ${actioned} for all of Ramones.`), + ).toBeInTheDocument(); + }); + }); describe.each` - accepted | dismissed | snoozed | action - ${customAcceptAlert} | ${null} | ${null} | ${['Accept', 'accepted']} - ${null} | ${customDismissAlert} | ${null} | ${['Dismiss', 'dismissed']} - ${null} | ${null} | ${customSnoozeAlert} | ${['Snooze', 'snoozed']} - `( - 'Custom forms', - ({ accepted, dismissed, snoozed, action: [Action, actioned] }) => { - it(`Displays a custom ${Action} form`, () => { - const { getByText } = render( - , - ); - expect(getByText(`You. ${Action}. Me.`)).toBeInTheDocument(); - expect(getByText(`${Action} this action item?`)).toBeInTheDocument(); - expect( - getByText(`This action item will be ${actioned} for all of Ramones.`), - ).toBeInTheDocument(); - }); - }, - ); + alert | status | action | text + ${customAcceptAlert} | ${AlertStatus.Accepted} | ${['accept', 'accepted']} | ${'My team can commit to making this change soon, or has already.'} + ${customDismissAlert} | ${AlertStatus.Dismissed} | ${['dismiss', 'dismissed']} | ${'Reason for dismissing?'} + ${customSnoozeAlert} | ${AlertStatus.Snoozed} | ${['snooze', 'snoozed']} | ${'For how long?'} + `('Custom forms', ({ alert, status, action: [action, actioned] }) => { + it(`Displays a custom ${capitalize(action)} form`, () => { + const { getByText } = render( + , + ); + expect(getByText(`You. ${capitalize(action)}. Me.`)).toBeInTheDocument(); + expect( + getByText(`${capitalize(action)} this action item?`), + ).toBeInTheDocument(); + expect( + getByText(`This action item will be ${actioned} for all of Ramones.`), + ).toBeInTheDocument(); + }); + }); describe.each` - accepted | dismissed | snoozed | action | text - ${nullAcceptAlert} | ${null} | ${null} | ${['Accept', 'accept', 'accepted']} | ${'My team can commit to making this change soon, or has already.'} - ${null} | ${nullDismissAlert} | ${null} | ${['Dismiss', 'dismiss', 'dismissed']} | ${'Reason for dismissing?'} - ${null} | ${null} | ${nullSnoozeAlert} | ${['Snooze', 'snooze', 'snoozed']} | ${'For how long?'} - `( - 'Null forms', - ({ - accepted, - dismissed, - snoozed, - action: [Action, action, actioned], - text, - }) => { - it(`Does NOT display a ${Action} form`, () => { - const { getByText, getByRole, queryByText } = render( - , - ); - expect(queryByText(text)).not.toBeInTheDocument(); - expect(getByRole('button', { name: action })).toBeInTheDocument(); - expect(getByText(`${Action} this action item?`)).toBeInTheDocument(); - expect( - getByText(`This action item will be ${actioned} for all of Ramones.`), - ).toBeInTheDocument(); - }); - }, - ); + alert | status | action | text + ${nullAcceptAlert} | ${AlertStatus.Accepted} | ${['accept', 'accepted']} | ${'My team can commit to making this change soon, or has already.'} + ${nullDismissAlert} | ${AlertStatus.Dismissed} | ${['dismiss', 'dismissed']} | ${'Reason for dismissing?'} + ${nullSnoozeAlert} | ${AlertStatus.Snoozed} | ${['snooze', 'snoozed']} | ${'For how long?'} + `('Null forms', ({ alert, status, action: [action, actioned], text }) => { + it(`Does NOT display a ${capitalize(action)} form`, () => { + const { getByText, getByRole, queryByText } = render( + , + ); + expect(queryByText(text)).not.toBeInTheDocument(); + expect(getByRole('button', { name: action })).toBeInTheDocument(); + expect( + getByText(`${capitalize(action)} this action item?`), + ).toBeInTheDocument(); + expect( + getByText(`This action item will be ${actioned} for all of Ramones.`), + ).toBeInTheDocument(); + }); + }); }); diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertDialog.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertDialog.tsx index f7154602b9..ab6f9fb9f0 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertDialog.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertDialog.tsx @@ -15,8 +15,8 @@ */ import React, { useEffect, useRef, useState } from 'react'; -import { default as CloseIcon } from '@material-ui/icons/Close'; import { + capitalize, Box, Button, Divider, @@ -26,23 +26,18 @@ import { DialogContent, Typography, } from '@material-ui/core'; -import { - AlertAcceptForm, - AlertDismissForm, - AlertSnoozeForm, -} from '../../forms'; +import { default as CloseIcon } from '@material-ui/icons/Close'; import { useAlertDialogStyles as useStyles } from '../../utils/styles'; -import { choose } from '../../utils/alerts'; -import { Alert, AlertForm, Maybe } from '../../types'; +import { Alert, AlertStatus, Maybe } from '../../types'; +import { choose, formOf } from '../../utils/alerts'; const DEFAULT_FORM_ID = 'alert-form'; type AlertDialogProps = { open: boolean; group: string; - snoozed: Maybe; - accepted: Maybe; - dismissed: Maybe; + alert: Maybe; + status: Maybe; onClose: () => void; onSubmit: (data: any) => void; }; @@ -50,87 +45,50 @@ type AlertDialogProps = { export const AlertDialog = ({ open, group, - snoozed, - accepted, - dismissed, + alert, + status, onClose, onSubmit, }: AlertDialogProps) => { const classes = useStyles(); - const [isButtonDisabled, setDisabled] = useState(true); - const acceptRef = useRef>(null); - const snoozeRef = useRef>(null); - const dismissRef = useRef>(null); + const [isSubmitDisabled, setSubmitDisabled] = useState(true); + const formRef = useRef>(null); useEffect(() => { - if (open) { - setDisabled(true); - } else { - setDisabled(false); - } + setSubmitDisabled(open); }, [open]); function disableSubmit(isDisabled: boolean) { - setDisabled(isDisabled); + setSubmitDisabled(isDisabled); } function onDialogClose() { onClose(); - setDisabled(true); + setSubmitDisabled(true); } - const SnoozeForm: Maybe = snoozed?.SnoozeForm ?? AlertSnoozeForm; - const AcceptForm: Maybe = accepted?.AcceptForm ?? AlertAcceptForm; - const DismissForm: Maybe = - dismissed?.DismissForm ?? AlertDismissForm; - - const isSnoozingEnabled = !!snoozed?.onSnoozed; - const isAcceptingEnabled = !!accepted?.onAccepted; - const isDismissingEnabled = !!dismissed?.onDismissed; - - const isSnoozeFormDisabled = snoozed?.SnoozeForm === null; - const isAcceptFormDisabled = accepted?.AcceptForm === null; - const isDismissFormDisabled = dismissed?.DismissForm === null; - const isFormDisabled = - isSnoozeFormDisabled || isAcceptFormDisabled || isDismissFormDisabled; - - const status = [ - isSnoozingEnabled, - isAcceptingEnabled, - isDismissingEnabled, - ] as const; - - const [Action, action, actioned] = - choose(status, [ - ['Snooze', 'snooze', 'snoozed'], - ['Accept', 'accept', 'accepted'], - ['Dismiss', 'dismiss', 'dismissed'], - ]) ?? []; - - const [title, subtitle] = - choose(status, [ - [snoozed?.title, snoozed?.subtitle], - [accepted?.title, accepted?.subtitle], - [dismissed?.title, dismissed?.subtitle], - ]) ?? []; + const [action, actioned] = choose( + status, + [ + ['snooze', 'snoozed'], + ['accept', 'accepted'], + ['dismiss', 'dismissed'], + ], + ['', ''], + ); const TransitionProps = { mountOnEnter: true, unmountOnExit: true, - // Wait for child component to mount; avoid recycling refs. onEntered() { - if (acceptRef.current) { - acceptRef.current.id = DEFAULT_FORM_ID; - } - if (snoozeRef.current) { - snoozeRef.current.id = DEFAULT_FORM_ID; - } - if (dismissRef.current) { - dismissRef.current.id = DEFAULT_FORM_ID; + if (formRef.current) { + formRef.current.id = DEFAULT_FORM_ID; } }, }; + const Form = formOf(alert, status); + return ( @@ -152,7 +110,7 @@ export const AlertDialog = ({ - {Action} this action item? + {capitalize(action)} this action item? @@ -169,30 +127,14 @@ export const AlertDialog = ({ borderRadius={4} > - {title} + {alert?.title} - {subtitle} + {alert?.subtitle} - {isSnoozingEnabled && !isSnoozeFormDisabled && ( - - )} - {isDismissingEnabled && !isDismissFormDisabled && ( - - )} - {isAcceptingEnabled && !isAcceptFormDisabled && ( - @@ -200,7 +142,18 @@ export const AlertDialog = ({ - {isFormDisabled ? ( + {Form ? ( + + ) : ( - ) : ( - )} diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertInsights.test.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertInsights.test.tsx index cd8444cb1f..98f75b1d41 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertInsights.test.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertInsights.test.tsx @@ -17,18 +17,12 @@ import React from 'react'; import { render, fireEvent, waitFor } from '@testing-library/react'; import { AlertInsights } from './AlertInsights'; -import { - MockScrollProvider, - MockAlertsProvider, - MockLoadingProvider, -} from '../../utils/tests'; +import { MockScrollProvider, MockLoadingProvider } from '../../utils/tests'; function renderInContext(children: JSX.Element) { return render( - - {children} - + {children} , ); } @@ -47,6 +41,7 @@ describe('', () => { snoozed={[]} accepted={[]} dismissed={[]} + onChange={jest.fn()} />, ); expect( @@ -70,6 +65,7 @@ describe('', () => { ]} accepted={[]} dismissed={[]} + onChange={jest.fn()} />, ); diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx index 1bcb684052..59b2dc9eec 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import pluralize from 'pluralize'; import { Box, Grid, Snackbar } from '@material-ui/core'; import { default as MuiAlert } from '@material-ui/lab/Alert'; @@ -24,15 +24,20 @@ import { AlertStatusSummaryButton } from './AlertStatusSummaryButton'; import { AlertInsightsHeader } from './AlertInsightsHeader'; import { AlertInsightsSection } from './AlertInsightsSection'; import { - useAlerts, useScroll, useLoading, ScrollType, MapLoadingToProps, } from '../../hooks'; import { DefaultLoadingAction } from '../../utils/loading'; -import { Alert, AlertOptions, Maybe } from '../../types'; -import { sumOfAllAlerts } from '../../utils/alerts'; +import { Alert, AlertOptions, AlertStatus, Maybe } from '../../types'; +import { + isStatusSnoozed, + isStatusAccepted, + isStatusDismissed, + sumOfAllAlerts, +} from '../../utils/alerts'; +import { ScrollAnchor } from '../../utils/scroll'; type MapLoadingtoAlerts = (isLoading: boolean) => void; @@ -47,6 +52,7 @@ type AlertInsightsProps = { snoozed: Alert[]; accepted: Alert[]; dismissed: Alert[]; + onChange: (alerts: Alert[]) => void; }; export const AlertInsights = ({ @@ -55,10 +61,12 @@ export const AlertInsights = ({ snoozed, accepted, dismissed, + onChange, }: AlertInsightsProps) => { - const [alerts, setAlerts] = useAlerts(); - const [scroll, , ScrollAnchor] = useScroll(); + const [scroll] = useScroll(); + const [alert, setAlert] = useState>(null); const dispatchLoadingAlerts = useLoading(mapLoadingToAlerts); + const [status, setStatus] = useState>(null); // Allow users to pass null values for data. const [data, setData] = useState>(undefined); const [error, setError] = useState>(null); @@ -66,22 +74,19 @@ export const AlertInsights = ({ const [isSummaryOpen, setSummaryOpen] = useState(false); const [isSnackbarOpen, setSnackbarOpen] = useState(false); - const closeDialog = useCallback(() => { - setData(undefined); - setDialogOpen(false); - setAlerts({ dismissed: null, snoozed: null, accepted: null }); - }, [setAlerts]); - useEffect(() => { - async function callHandler( + async function callAlertHook( options: AlertOptions, callback: (options: AlertOptions) => Promise, ) { - closeDialog(); + setAlert(null); + setStatus(null); + setData(undefined); + setDialogOpen(false); dispatchLoadingAlerts(true); try { - const a: Alert[] = await callback(options); - setAlerts({ alerts: a }); + const alerts: Alert[] = await callback(options); + onChange(alerts); } catch (e) { setError(e); } finally { @@ -90,22 +95,20 @@ export const AlertInsights = ({ } const options: AlertOptions = { data, group }; - const onSnoozed = alerts.snoozed?.onSnoozed?.bind(alerts.snoozed) ?? null; - const onAccepted = - alerts.accepted?.onAccepted?.bind(alerts.accepted) ?? null; - const onDismissed = - alerts.dismissed?.onDismissed?.bind(alerts.dismissed) ?? null; + const onSnoozed = alert?.onSnoozed?.bind(alert); + const onAccepted = alert?.onAccepted?.bind(alert); + const onDismissed = alert?.onDismissed?.bind(alert); if (data !== undefined) { - if (onSnoozed) { - callHandler(options, onSnoozed); - } else if (onAccepted) { - callHandler(options, onAccepted); - } else if (onDismissed) { - callHandler(options, onDismissed); + if (isStatusSnoozed(status) && onSnoozed) { + callAlertHook(options, onSnoozed); + } else if (isStatusAccepted(status) && onAccepted) { + callAlertHook(options, onAccepted); + } else if (isStatusDismissed(status) && onDismissed) { + callAlertHook(options, onDismissed); } } - }, [group, data, alerts, setAlerts, closeDialog, dispatchLoadingAlerts]); + }, [group, data, alert, status, onChange, dispatchLoadingAlerts]); useEffect(() => { if (scroll === ScrollType.AlertSummary) { @@ -114,34 +117,38 @@ export const AlertInsights = ({ }, [scroll]); useEffect(() => { - if (error) { - setSnackbarOpen(true); - } else { - setSnackbarOpen(false); - } - }, [error]); + setDialogOpen(!!status); + }, [status]); useEffect(() => { - function toggleDialogOnStatusChange() { - const isAlertSnoozed = !!alerts.snoozed; - const isAlertAccepted = !!alerts.accepted; - const isAlertDismissed = !!alerts.dismissed; + setSnackbarOpen(!!error); + }, [error]); - if (isAlertSnoozed || isAlertDismissed || isAlertAccepted) { - setDialogOpen(true); - } else { - setDialogOpen(false); - } - } + function onSnooze(alert: Alert) { + setAlert(alert); + setStatus(AlertStatus.Snoozed); + } - toggleDialogOnStatusChange(); - }, [alerts.snoozed, alerts.dismissed, alerts.accepted]); + function onAccept(alert: Alert) { + setAlert(alert); + setStatus(AlertStatus.Accepted); + } + + function onDismiss(alert: Alert) { + setAlert(alert); + setStatus(AlertStatus.Dismissed); + } function onSnackbarClose() { setError(null); } - function onDialogSubmit(data: any) { + function onDialogClose() { + setAlert(null); + setStatus(null); + } + + function onDialogFormSubmit(data: any) { setData(data); } @@ -153,7 +160,6 @@ export const AlertInsights = ({ const isAlertStatusSummaryDisplayed = !!total; const isAlertInsightSectionDisplayed = !!active.length; - // AlertInsights will not display if there aren't any active or hidden items. return ( @@ -171,7 +177,13 @@ export const AlertInsights = ({ {active.map((alert, index) => ( - + ))} @@ -195,11 +207,10 @@ export const AlertInsights = ({ { const classes = useStyles(); - const [, , ScrollAnchor] = useScroll(); return ( diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.test.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.test.tsx index f04e1d8b01..937e099356 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.test.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.test.tsx @@ -17,8 +17,7 @@ import React from 'react'; import { AlertInsightsSection } from './AlertInsightsSection'; import { render } from '@testing-library/react'; import { Alert } from '../../types'; -import { AlertState } from '../../hooks'; -import { MockScrollProvider, MockAlertsProvider } from '../../utils/tests'; +import { MockScrollProvider } from '../../utils/tests'; const mockAlert: Alert = { subtitle: @@ -27,14 +26,20 @@ const mockAlert: Alert = { url: '/cost-insights/test', }; +function renderInContext(children: JSX.Element) { + return render({children}); +} + describe('', () => { it('Renders alert without exploding', () => { - const { getByText, queryByText } = render( - - - - - , + const { getByText, queryByText } = renderInContext( + , ); expect(getByText(mockAlert.title)).toBeInTheDocument(); expect(getByText(mockAlert.subtitle)).toBeInTheDocument(); @@ -49,12 +54,14 @@ describe('', () => { ...mockAlert, url: undefined, }; - const { queryByText } = render( - - - - - , + const { queryByText } = renderInContext( + , ); expect(queryByText('View Instructions')).not.toBeInTheDocument(); }); @@ -65,19 +72,14 @@ describe('', () => { onSnoozed: jest.fn(), }; - const context: AlertState = { - alerts: [], - snoozed: alert, - dismissed: null, - accepted: null, - }; - - const { queryByText, getByText } = render( - - - - - , + const { queryByText, getByText } = renderInContext( + , ); expect(getByText('Snooze')).toBeInTheDocument(); @@ -90,19 +92,15 @@ describe('', () => { ...mockAlert, onDismissed: jest.fn(), }; - const context: AlertState = { - alerts: [], - snoozed: null, - dismissed: alert, - accepted: null, - }; - const { queryByText, getByText } = render( - - - - - , + const { queryByText, getByText } = renderInContext( + , ); expect(getByText('Dismiss')).toBeInTheDocument(); @@ -116,19 +114,14 @@ describe('', () => { onAccepted: jest.fn(), }; - const context: AlertState = { - alerts: [], - snoozed: null, - dismissed: null, - accepted: alert, - }; - - const { queryByText, getByText } = render( - - - - - , + const { queryByText, getByText } = renderInContext( + , ); expect(getByText('Accept')).toBeInTheDocument(); diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.tsx index dda2d66f3f..d52687ae19 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.tsx @@ -19,23 +19,31 @@ import { default as SnoozeIcon } from '@material-ui/icons/AccessTime'; import { default as AcceptIcon } from '@material-ui/icons/Check'; import { default as DismissIcon } from '@material-ui/icons/Delete'; import { AlertInsightsSectionHeader } from './AlertInsightsSectionHeader'; -import { useAlerts } from '../../hooks'; import { Alert } from '../../types'; +import { + isSnoozeEnabled, + isAcceptEnabled, + isDismissEnabled, +} from '../../utils/alerts'; type AlertInsightsSectionProps = { alert: Alert; number: number; + onSnooze: (alert: Alert) => void; + onAccept: (alert: Alert) => void; + onDismiss: (alert: Alert) => void; }; export const AlertInsightsSection = ({ alert, number, + onSnooze, + onAccept, + onDismiss, }: AlertInsightsSectionProps) => { - const [, setAlerts] = useAlerts(); - - const isSnoozeButtonDisplayed = !!alert.onSnoozed; - const isAcceptButtonDisplayed = !!alert.onAccepted; - const isDismissButtonDisplayed = !!alert.onDismissed; + const isSnoozeButtonDisplayed = isSnoozeEnabled(alert); + const isAcceptButtonDisplayed = isAcceptEnabled(alert); + const isDismissButtonDisplayed = isDismissEnabled(alert); const isButtonGroupDisplayed = isSnoozeButtonDisplayed || isAcceptButtonDisplayed || @@ -52,7 +60,7 @@ export const AlertInsightsSection = ({ color="primary" variant="contained" aria-label="accept" - onClick={() => setAlerts({ accepted: alert })} + onClick={() => onAccept(alert)} startIcon={} > Accept @@ -66,7 +74,7 @@ export const AlertInsightsSection = ({ variant="outlined" aria-label="snooze" disableElevation - onClick={() => setAlerts({ snoozed: alert })} + onClick={() => onSnooze(alert)} startIcon={} > Snooze @@ -79,7 +87,7 @@ export const AlertInsightsSection = ({ variant="outlined" aria-label="dismiss" disableElevation - onClick={() => setAlerts({ dismissed: alert })} + onClick={() => onDismiss(alert)} startIcon={} > Dismiss diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSectionHeader.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSectionHeader.tsx index c463a05609..30b8985934 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSectionHeader.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSectionHeader.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { Avatar, Box, Button, Grid, Typography } from '@material-ui/core'; import { useAlertInsightsSectionStyles as useStyles } from '../../utils/styles'; -import { useScroll } from '../../hooks'; +import { ScrollAnchor } from '../../utils/scroll'; import { Alert } from '../../types'; type AlertInsightsSectionHeaderProps = { @@ -29,7 +29,6 @@ export const AlertInsightsSectionHeader = ({ alert, number, }: AlertInsightsSectionHeaderProps) => { - const [, , ScrollAnchor] = useScroll(); const classes = useStyles(); const isViewInstructionsButtonDisplayed = !!alert.url; diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertStatusSummary.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertStatusSummary.tsx index cb0a149511..caee99dc5e 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertStatusSummary.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertStatusSummary.tsx @@ -15,7 +15,7 @@ */ import React, { Fragment } from 'react'; -import { Avatar, Box, Collapse, Divider } from '@material-ui/core'; +import { Avatar, Box, Collapse, Divider, Tooltip } from '@material-ui/core'; import { default as AcceptIcon } from '@material-ui/icons/Check'; import { default as DismissIcon } from '@material-ui/icons/Delete'; import { default as SnoozeIcon } from '@material-ui/icons/AccessTime'; @@ -23,6 +23,35 @@ import { ActionItemCard } from '../ActionItems'; import { Alert, AlertStatus } from '../../types'; import { useActionItemCardStyles as useStyles } from '../../utils/styles'; +type AlertGroupProps = { + alerts: Alert[]; + status: AlertStatus; + title: string; + icon: JSX.Element; +}; + +const AlertGroup = ({ alerts, status, title, icon }: AlertGroupProps) => { + const classes = useStyles(); + return ( + + {alerts.map((alert, index) => ( + + + {icon} + + } + /> + {index < alerts.length - 1 && } + + ))} + + ); +}; + type AlertStatusSummaryProps = { open: boolean; snoozed: Alert[]; @@ -36,8 +65,6 @@ export const AlertStatusSummary = ({ accepted, dismissed, }: AlertStatusSummaryProps) => { - const classes = useStyles(); - const isSnoozedListDisplayed = !!snoozed.length; const isAcceptedListDisplayed = !!accepted.length; const isDismissedListDisplayed = !!dismissed.length; @@ -45,71 +72,46 @@ export const AlertStatusSummary = ({ return ( {isAcceptedListDisplayed && ( - - {accepted.map((alert, index) => ( - - - {/* Icons indicate alert status. Do not hide from accesibility tree */} - - - } - /> - {index < accepted.length - 1 && } - - ))} - + + } + /> )} {isSnoozedListDisplayed && ( - - {snoozed.map((alert, index) => ( - - - - - } - /> - {index < snoozed.length - 1 && } - - ))} - + + } + /> )} {isDismissedListDisplayed && ( - - {dismissed.map((alert, index) => ( - - - - - } - /> - {index < dismissed.length - 1 && } - - ))} - + + } + /> )} ); diff --git a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx index f974fa0ddd..a8ba454d11 100644 --- a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx +++ b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx @@ -44,22 +44,21 @@ import { ProductInsights } from '../ProductInsights'; import { useConfig, useCurrency, - useAlerts, useFilters, useGroups, useLastCompleteBillingDate, useLoading, } from '../../hooks'; -import { Cost, Maybe, MetricData, Product, Project } from '../../types'; +import { Alert, Cost, Maybe, MetricData, Product, Project } from '../../types'; import { mapLoadingToProps } from './selector'; import { ProjectSelect } from '../ProjectSelect'; import { intervalsOf } from '../../utils/duration'; import { useSubtleTypographyStyles } from '../../utils/styles'; import { - isActive, - isAccepted, - isDismissed, - isSnoozed, + isAlertActive, + isAlertAccepted, + isAlertDismissed, + isAlertSnoozed, } from '../../utils/alerts'; export const CostInsightsPage = () => { @@ -68,7 +67,7 @@ export const CostInsightsPage = () => { const config = useConfig(); const groups = useGroups(); const lastCompleteBillingDate = useLastCompleteBillingDate(); - const [alerts, setAlerts] = useAlerts(); + const [alerts, setAlerts] = useState([]); const [currency, setCurrency] = useCurrency(); const [projects, setProjects] = useState>(null); const [products, setProducts] = useState>(null); @@ -78,21 +77,13 @@ export const CostInsightsPage = () => { const { pageFilters, setPageFilters } = useFilters(p => p); - const snoozed = useMemo(() => alerts.alerts.filter(isSnoozed), [ - alerts.alerts, - ]); - const accepted = useMemo(() => alerts.alerts.filter(isAccepted), [ - alerts.alerts, - ]); - const dismissed = useMemo(() => alerts.alerts.filter(isDismissed), [ - alerts.alerts, - ]); - const activeAlerts = useMemo(() => alerts.alerts.filter(isActive), [ - alerts.alerts, - ]); + const active = useMemo(() => alerts.filter(isAlertActive), [alerts]); + const snoozed = useMemo(() => alerts.filter(isAlertSnoozed), [alerts]); + const accepted = useMemo(() => alerts.filter(isAlertAccepted), [alerts]); + const dismissed = useMemo(() => alerts.filter(isAlertDismissed), [alerts]); - const isActionItemsDisplayed = !!activeAlerts.length; - const isAlertInsightsDisplayed = !!alerts.alerts.length; + const isActionItemsDisplayed = !!active.length; + const isAlertInsightsDisplayed = !!alerts.length; const { loadingActions, @@ -150,7 +141,7 @@ export const CostInsightsPage = () => { : client.getGroupDailyCost(pageFilters.group, intervals), ]); setProjects(fetchedProjects); - setAlerts({ alerts: fetchedAlerts }); + setAlerts(fetchedAlerts); setMetricData(fetchedMetricData); setDailyCost(fetchedDailyCost); } else { @@ -175,7 +166,6 @@ export const CostInsightsPage = () => { loadingActions, loadingGroups, loadingBillingDate, - setAlerts, dispatchLoadingInsights, dispatchLoadingInitial, dispatchLoadingNone, @@ -259,7 +249,7 @@ export const CostInsightsPage = () => { @@ -280,14 +270,14 @@ export const CostInsightsPage = () => { owner={pageFilters.group} groups={groups} hasCostData={!!dailyCost.aggregation.length} - alerts={activeAlerts.length} + alerts={active.length} /> { diff --git a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPageRoot.tsx b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPageRoot.tsx index 1947ca185e..4f506b4076 100644 --- a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPageRoot.tsx +++ b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPageRoot.tsx @@ -20,7 +20,6 @@ import { FilterProvider } from '../../hooks/useFilters'; import { LoadingProvider } from '../../hooks/useLoading'; import { GroupsProvider } from '../../hooks/useGroups'; import { CurrencyProvider } from '../../hooks/useCurrency'; -import { AlertsProvider } from '../../hooks/useAlerts'; import { ScrollProvider } from '../../hooks/useScroll'; import { ConfigProvider } from '../../hooks/useConfig'; import { BillingDateProvider } from '../../hooks/useLastCompleteBillingDate'; @@ -35,9 +34,7 @@ export const CostInsightsPageRoot = () => ( - - - + diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx index aedec33457..85a9a9db74 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx @@ -30,12 +30,13 @@ import { CostOverviewBreakdownChart } from './CostOverviewBreakdownChart'; import { CostOverviewHeader } from './CostOverviewHeader'; import { MetricSelect } from '../MetricSelect'; import { PeriodSelect } from '../PeriodSelect'; -import { useConfig, useFilters, useScroll } from '../../hooks'; +import { useConfig, useFilters } from '../../hooks'; import { mapFiltersToProps } from './selector'; import { DefaultNavigation } from '../../utils/navigation'; import { findAlways } from '../../utils/assert'; import { Cost, CostInsightsTheme, Maybe, MetricData } from '../../types'; import { useOverviewTabsStyles } from '../../utils/styles'; +import { ScrollAnchor } from '../../utils/scroll'; export type CostOverviewCardProps = { dailyCostData: Cost; @@ -49,7 +50,6 @@ export const CostOverviewCard = ({ const theme = useTheme(); const styles = useOverviewTabsStyles(theme); const config = useConfig(); - const [, , ScrollAnchor] = useScroll(); const [tabIndex, setTabIndex] = useState(0); const { setDuration, setProject, setMetric, ...filters } = useFilters( mapFiltersToProps, diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx index 4626954bac..d46737cafb 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx @@ -34,9 +34,9 @@ import { MapLoadingToProps, useLastCompleteBillingDate, useLoading, - useScroll, } from '../../hooks'; import { findAnyKey } from '../../utils/assert'; +import { ScrollAnchor } from '../../utils/scroll'; type LoadingProps = (isLoading: boolean) => void; @@ -60,7 +60,6 @@ export const ProductInsightsCard = ({ }: PropsWithChildren) => { const classes = useStyles(); const mountedRef = useRef(false); - const [, , ScrollAnchor] = useScroll(); const [error, setError] = useState>(null); const dispatchLoading = useLoading(mapLoadingToProps); const lastCompleteBillingDate = useLastCompleteBillingDate(); diff --git a/plugins/cost-insights/src/forms/AlertSnoozeForm.tsx b/plugins/cost-insights/src/forms/AlertSnoozeForm.tsx index 51218fe844..743b5ddac1 100644 --- a/plugins/cost-insights/src/forms/AlertSnoozeForm.tsx +++ b/plugins/cost-insights/src/forms/AlertSnoozeForm.tsx @@ -24,7 +24,6 @@ import React, { import dayjs from 'dayjs'; import { Box, - Collapse, FormControl, FormControlLabel, RadioGroup, @@ -50,19 +49,18 @@ export const AlertSnoozeForm = forwardRef< AlertSnoozeFormProps >(({ onSubmit, disableSubmit }, ref) => { const classes = useStyles(); - const [error, setError] = useState>(null); const [duration, setDuration] = useState>(Duration.P7D); + useEffect(() => disableSubmit(false), [disableSubmit]); + const onFormSubmit: FormEventHandler = e => { e.preventDefault(); if (duration) { const repeatInterval = 1; - const inclusiveEndDate = dayjs().format(DEFAULT_DATE_FORMAT); + const today = dayjs().format(DEFAULT_DATE_FORMAT); onSubmit({ - intervals: intervalsOf(duration, inclusiveEndDate, repeatInterval), + intervals: intervalsOf(duration, today, repeatInterval), }); - } else { - setError(new Error('Please select an option.')); } }; @@ -73,26 +71,12 @@ export const AlertSnoozeForm = forwardRef< setDuration(value as Duration); }; - useEffect(() => { - function clearErrorOnFormDataChange() { - disableSubmit(false); - setError(prevError => (prevError ? null : prevError)); - } - - clearErrorOnFormDataChange(); - }, [duration, disableSubmit]); - - const isErrorMessageDisplayed = !!error; - return (
- + For how long? - - {error?.message} - >>; -}; - -export const AlertsContext = createContext( - undefined, -); - -export type AlertState = { - alerts: Alert[]; - snoozed: Maybe; - accepted: Maybe; - dismissed: Maybe; -}; - -const initialState: AlertState = { - alerts: [], - snoozed: null, - accepted: null, - dismissed: null, -}; - -const reducer = ( - prevState: AlertState, - action: SetStateAction>, -): AlertState => ({ - ...prevState, - ...action, -}); - -export const AlertsProvider = ({ children }: PropsWithChildren<{}>) => { - const [alerts, setAlerts] = useReducer(reducer, initialState); - - return ( - - {children} - - ); -}; - -export function useAlerts() { - const context = useContext(AlertsContext); - return context - ? ([context.alerts, context.setAlerts] as const) - : assertNever(); -} - -function assertNever(): never { - throw new Error('useAlerts cannot be used outside AlertsContext provider'); -} diff --git a/plugins/cost-insights/src/hooks/useScroll.tsx b/plugins/cost-insights/src/hooks/useScroll.tsx index 7f310d59b0..137762cc2d 100644 --- a/plugins/cost-insights/src/hooks/useScroll.tsx +++ b/plugins/cost-insights/src/hooks/useScroll.tsx @@ -15,12 +15,9 @@ */ import React, { Dispatch, - ElementType, SetStateAction, useState, useContext, - useEffect, - useRef, PropsWithChildren, } from 'react'; import { Maybe } from '../types'; @@ -30,61 +27,16 @@ export type ScrollTo = Maybe; export type ScrollContextProps = { scroll: ScrollTo; setScroll: Dispatch>; - ScrollAnchor: ElementType; }; -export interface ScrollAnchorProps extends ScrollIntoViewOptions { - id: ScrollTo; - top?: number; - left?: number; -} - export const ScrollContext = React.createContext< ScrollContextProps | undefined >(undefined); -export const ScrollAnchor = ({ - id, - block, - inline, - left = 0, - top = -20, - behavior = 'smooth', -}: ScrollAnchorProps) => { - const divRef = useRef(null); - const [scroll, setScroll] = useScroll(); - - useEffect(() => { - function scrollIntoView() { - const options = { - behavior: behavior || 'auto', - block: block || 'start', - inline: inline || 'nearest', - }; - - if (divRef.current && scroll === id) { - divRef.current.scrollIntoView(options); - setScroll(null); - } - } - - scrollIntoView(); - }, [scroll, setScroll, id, behavior, block, inline]); - - return ( -
- ); -}; - export const ScrollProvider = ({ children }: PropsWithChildren<{}>) => { const [scroll, setScroll] = useState(null); - return ( - + {children} ); @@ -101,11 +53,9 @@ export function useScroll() { assertNever(); } - return [context.scroll, context.setScroll, context.ScrollAnchor] as const; + return [context.scroll, context.setScroll] as const; } function assertNever(): never { - throw new Error( - `Cannot use useScroll or ScrollAnchor outside ScrollProvider`, - ); + throw new Error(`Cannot use useScroll outside ScrollProvider`); } diff --git a/plugins/cost-insights/src/utils/alerts.test.tsx b/plugins/cost-insights/src/utils/alerts.test.tsx new file mode 100644 index 0000000000..d821fd9f1f --- /dev/null +++ b/plugins/cost-insights/src/utils/alerts.test.tsx @@ -0,0 +1,109 @@ +/* + * 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, { ReactNode } from 'react'; +import { formOf } from './alerts'; +import { AlertAcceptForm, AlertDismissForm, AlertSnoozeForm } from '../forms'; +import { Alert, AlertStatus, AlertFormProps } from '../types'; + +type Props = AlertFormProps; + +const createMockForm = (children: ReactNode) => + React.forwardRef((props, ref) => ( + + {children} + + )); + +const snoozeDefault: Alert = { + title: 'title', + subtitle: 'subtitle', + onSnoozed: jest.fn(), +}; + +const snoozeCustom: Alert = { + title: 'title', + subtitle: 'subtitle', + onSnoozed: jest.fn(), + SnoozeForm: createMockForm('Snooze'), +}; + +const snoozeNull: Alert = { + title: 'title', + subtitle: 'subtitle', + onSnoozed: jest.fn(), + SnoozeForm: null, +}; + +const acceptDefault: Alert = { + title: 'title', + subtitle: 'subtitle', + onAccepted: jest.fn(), +}; + +const acceptCustom: Alert = { + title: 'title', + subtitle: 'subtitle', + onAccepted: jest.fn(), + AcceptForm: createMockForm('Accept'), +}; + +const acceptNull: Alert = { + title: 'title', + subtitle: 'subtitle', + onAccepted: jest.fn(), + AcceptForm: null, +}; + +const dismissDefault: Alert = { + title: 'title', + subtitle: 'subtitle', + onDismissed: jest.fn(), +}; + +const dismissCustom: Alert = { + title: 'title', + subtitle: 'subtitle', + onDismissed: jest.fn(), + DismissForm: createMockForm('Dismiss'), +}; + +const dismissNull: Alert = { + title: 'title', + subtitle: 'subtitle', + onDismissed: jest.fn(), + DismissForm: null, +}; + +describe('formOf', () => { + describe.each` + msg | alert | status | expected + ${'default snooze form'} | ${snoozeDefault} | ${AlertStatus.Snoozed} | ${AlertSnoozeForm} + ${'custom snooze form'} | ${snoozeCustom} | ${AlertStatus.Snoozed} | ${snoozeCustom.SnoozeForm} + ${'null snooze form'} | ${snoozeNull} | ${AlertStatus.Snoozed} | ${null} + ${'default accept form'} | ${acceptDefault} | ${AlertStatus.Accepted} | ${AlertAcceptForm} + ${'custom accept form'} | ${acceptCustom} | ${AlertStatus.Accepted} | ${acceptCustom.AcceptForm} + ${'null accept form'} | ${acceptNull} | ${AlertStatus.Accepted} | ${null} + ${'default dismiss form'} | ${dismissDefault} | ${AlertStatus.Dismissed} | ${AlertDismissForm} + ${'custom dismiss form'} | ${dismissCustom} | ${AlertStatus.Dismissed} | ${dismissCustom.DismissForm} + ${'null dismiss form'} | ${dismissNull} | ${AlertStatus.Dismissed} | ${null} + ${'no form or status'} | ${null} | ${null} | ${null} + `('Should render the correct form', ({ msg, alert, status, expected }) => { + it(`for ${msg}`, () => { + const result = formOf(alert, status); + expect(result).toBe(expected); + }); + }); +}); diff --git a/plugins/cost-insights/src/utils/alerts.tsx b/plugins/cost-insights/src/utils/alerts.tsx index e10740fbfe..56403a664c 100644 --- a/plugins/cost-insights/src/utils/alerts.tsx +++ b/plugins/cost-insights/src/utils/alerts.tsx @@ -14,22 +14,118 @@ * limitations under the License. */ -import { Alert, AlertStatus } from '../types'; +import { Alert, AlertForm, AlertStatus, Maybe } from '../types'; +import { AlertAcceptForm, AlertDismissForm, AlertSnoozeForm } from '../forms'; -const createStatusHandler = (status?: string) => (alert: Alert) => +const createAlertHandler = (status?: AlertStatus) => (alert: Alert) => alert.status === status; -export const isActive = createStatusHandler(); -export const isSnoozed = createStatusHandler(AlertStatus.Snoozed); -export const isAccepted = createStatusHandler(AlertStatus.Accepted); -export const isDismissed = createStatusHandler(AlertStatus.Dismissed); +export const isAlertActive = (alert: Alert) => !hasProperty(alert, 'status'); +export const isAlertSnoozed = createAlertHandler(AlertStatus.Snoozed); +export const isAlertAccepted = createAlertHandler(AlertStatus.Accepted); +export const isAlertDismissed = createAlertHandler(AlertStatus.Dismissed); + +const createStatusHandler = (status: AlertStatus) => (s: Maybe) => + s === status; +export const isStatusSnoozed = createStatusHandler(AlertStatus.Snoozed); +export const isStatusAccepted = createStatusHandler(AlertStatus.Accepted); +export const isStatusDismissed = createStatusHandler(AlertStatus.Dismissed); + +const createAlertEventHandler = ( + onEvent: 'onSnoozed' | 'onAccepted' | 'onDismissed', +) => (alert: Maybe): boolean => hasProperty(alert, onEvent); +export const isSnoozeEnabled = createAlertEventHandler('onSnoozed'); +export const isAcceptEnabled = createAlertEventHandler('onAccepted'); +export const isDismissEnabled = createAlertEventHandler('onDismissed'); + +const createFormEnabledHandler = ( + Form: 'SnoozeForm' | 'AcceptForm' | 'DismissForm', +) => (alert: Maybe): boolean => { + if (!alert) return false; + if (alert[Form] === null) return false; + switch (Form) { + case 'SnoozeForm': + return isSnoozeEnabled(alert); + case 'AcceptForm': + return isAcceptEnabled(alert); + case 'DismissForm': + return isDismissEnabled(alert); + default: + return false; + } +}; +export const isSnoozeFormEnabled = createFormEnabledHandler('SnoozeForm'); +export const isAcceptFormEnabled = createFormEnabledHandler('AcceptForm'); +export const isDismissFormEnabled = createFormEnabledHandler('DismissForm'); + +/** + * Utility for determining if a form is disabled. + * When a form is disabled, the dialog button's type should convert from submit to button. + * @param alert + * @param status + */ +export const isFormDisabled = ( + alert: Maybe, + status: Maybe, +): boolean => { + switch (status) { + case AlertStatus.Snoozed: + return alert?.SnoozeForm === null; + case AlertStatus.Accepted: + return alert?.AcceptForm === null; + case AlertStatus.Dismissed: + return alert?.DismissForm === null; + default: + return false; + } +}; + +export function formOf( + alert: Maybe, + status: Maybe, +): Maybe { + switch (status) { + case AlertStatus.Snoozed: { + const SnoozeForm = alert?.SnoozeForm ?? AlertSnoozeForm; + return isSnoozeFormEnabled(alert) ? SnoozeForm : null; + } + case AlertStatus.Accepted: { + const AcceptForm = alert?.AcceptForm ?? AlertAcceptForm; + return isAcceptFormEnabled(alert) ? AcceptForm : null; + } + case AlertStatus.Dismissed: { + const DismissForm = alert?.DismissForm ?? AlertDismissForm; + return isDismissFormEnabled(alert) ? DismissForm : null; + } + default: + return null; + } +} + +/** + * Utility for choosing from a fixed set of values for a given alert status. + * @param status + * @param values + */ +export function choose( + status: Maybe, + values: [T, T, T], + none: T, +): T { + switch (status) { + case AlertStatus.Snoozed: + return values[0]; + case AlertStatus.Accepted: + return values[1]; + case AlertStatus.Dismissed: + return values[2]; + default: + return none; + } +} + +export function hasProperty(alert: Maybe, prop: keyof Alert): boolean { + return prop in (alert ?? {}); +} export const sumOfAllAlerts = (sum: number, alerts: Alert[]) => sum + alerts.length; - -export function choose( - status: readonly [boolean, boolean, boolean], - values: [T, T, T], -): T | undefined { - const i = status.indexOf(true); - return values[i]; -} diff --git a/plugins/cost-insights/src/utils/scroll.tsx b/plugins/cost-insights/src/utils/scroll.tsx new file mode 100644 index 0000000000..06f01775df --- /dev/null +++ b/plugins/cost-insights/src/utils/scroll.tsx @@ -0,0 +1,65 @@ +/* + * 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, useRef } from 'react'; +import { ScrollTo, useScroll } from '../hooks/useScroll'; + +/* + Utility component use in conjuction with useScroll that allows scrollable components to control behavior and offset. + 1. ScrollAnchor must be a direct child of a scrollable component. + 2. ScrollAnchor's parent position must be relative. + 3. ScrollAnchor's id must be unique. +*/ + +export interface ScrollAnchorProps extends ScrollIntoViewOptions { + id: ScrollTo; + top?: number; + left?: number; +} + +export const ScrollAnchor = ({ + id, + left = 0, + top = -20, + block = 'start', + inline = 'nearest', + behavior = 'smooth', +}: ScrollAnchorProps) => { + const divRef = useRef(null); + const [scroll, setScroll] = useScroll(); + + useEffect(() => { + function scrollIntoView() { + if (divRef.current && scroll === id) { + divRef.current.scrollIntoView({ + block, + inline, + behavior, + }); + setScroll(null); + } + } + + scrollIntoView(); + }, [scroll, setScroll, id, behavior, block, inline]); + + return ( +
+ ); +}; diff --git a/plugins/cost-insights/src/utils/tests.tsx b/plugins/cost-insights/src/utils/tests.tsx index 13b38a49f2..aae2800573 100644 --- a/plugins/cost-insights/src/utils/tests.tsx +++ b/plugins/cost-insights/src/utils/tests.tsx @@ -22,7 +22,6 @@ import { IdentityApi, identityApiRef, } from '@backstage/core'; -import { AlertsContext, AlertsContextProps } from '../hooks/useAlerts'; import { LoadingContext, LoadingContextProps } from '../hooks/useLoading'; import { GroupsContext, GroupsContextProps } from '../hooks/useGroups'; import { FilterContext, FilterContextProps } from '../hooks/useFilters'; @@ -166,7 +165,6 @@ export const MockScrollProvider = ({ children }: MockScrollProviderProps) => { const defaultContext: ScrollContextProps = { scroll: null, setScroll: jest.fn(), - ScrollAnchor: jest.fn(() =>
), }; return ( @@ -233,28 +231,3 @@ export const MockCostInsightsApiProvider = ({ return {children}; }; - -export type MockAlertsProviderContextProps = PartialPropsWithChildren< - AlertsContextProps ->; - -export const MockAlertsProvider = ({ - children, - ...context -}: MockAlertsProviderContextProps) => { - const defaultContext: AlertsContextProps = { - alerts: { - alerts: [], - snoozed: null, - accepted: null, - dismissed: null, - }, - setAlerts: jest.fn(), - }; - - return ( - - {children} - - ); -}; From 03177610a8afb32f8e6e8c7de8d3bc8a011e21d0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 Jan 2021 16:58:18 +0100 Subject: [PATCH 46/99] cli: add fix for module resolution in backend bundle config --- packages/cli/src/lib/bundler/config.ts | 31 +++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index c53a8329f5..6fbcc65c9a 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -219,7 +219,7 @@ export async function createBackendConfig( } : {}), externals: [ - nodeExternals({ + nodeExternalsWithResolve({ modulesDir: paths.rootNodeModules, additionalModuleDirs: moduleDirs, allowlist: ['webpack/hot/poll?100', ...localPackageNames], @@ -296,3 +296,32 @@ export async function createBackendConfig( ], }; } + +// This makes the module resolution happen from the context of each non-external module, rather +// than the main entrypoint. This fixes a bug where dependencies would be resolved from the backend +// package rather than each individual backend package and plugin. +// +// TODO(Rugvip): Feature suggestion/contribute this to webpack-externals +function nodeExternalsWithResolve( + options: Parameters[0], +) { + let currentContext: string; + const externals = nodeExternals({ + ...options, + importType(request) { + const resolved = require.resolve(request, { + paths: [currentContext], + }); + return `commonjs ${resolved}`; + }, + }); + + return ( + context: string, + request: string, + callback: webpack.ExternalsFunctionCallback, + ) => { + currentContext = context; + return externals(context, request, callback); + }; +} From bdbeaf605106cb8e2f2082950bb4a2d23ed1b2d7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 Jan 2021 16:58:48 +0100 Subject: [PATCH 47/99] config-loader: validate each schema upfront to provide a more precise error message --- packages/config-loader/src/lib/schema/compile.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/config-loader/src/lib/schema/compile.ts b/packages/config-loader/src/lib/schema/compile.ts index 2607d51f80..e340d775a1 100644 --- a/packages/config-loader/src/lib/schema/compile.ts +++ b/packages/config-loader/src/lib/schema/compile.ts @@ -69,6 +69,14 @@ export function compileConfigSchemas( }, }); + for (const schema of schemas) { + try { + ajv.compile(schema.value); + } catch (error) { + throw new Error(`Schema at ${schema.path} is invalid, ${error}`); + } + } + const merged = mergeAllOf( { allOf: schemas.map(_ => _.value) }, { From 9f2b9c2020a55bd103ef7ca2106d61bc9f9d04a1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 Jan 2021 16:59:19 +0100 Subject: [PATCH 48/99] app-backend: re-throw config loading errors with instructions for how to fix --- plugins/app-backend/src/lib/config.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/plugins/app-backend/src/lib/config.ts b/plugins/app-backend/src/lib/config.ts index 99590c1e2d..0607956894 100644 --- a/plugins/app-backend/src/lib/config.ts +++ b/plugins/app-backend/src/lib/config.ts @@ -84,13 +84,21 @@ export async function readConfigs(options: ReadOptions): Promise { const schemaPath = resolvePath(appDistDir, '.config-schema.json'); if (await fs.pathExists(schemaPath)) { const serializedSchema = await fs.readJson(schemaPath); - const schema = await loadConfigSchema({ serialized: serializedSchema }); - const frontendConfigs = await schema.process( - [{ data: config.get() as JsonObject, context: 'app' }], - { visibility: ['frontend'] }, - ); - appConfigs.push(...frontendConfigs); + try { + const schema = await loadConfigSchema({ serialized: serializedSchema }); + + const frontendConfigs = await schema.process( + [{ data: config.get() as JsonObject, context: 'app' }], + { visibility: ['frontend'] }, + ); + appConfigs.push(...frontendConfigs); + } catch (error) { + throw new Error( + 'Invalid schema embedded in the app bundle, to fix this issue you need ' + + `to correct the schema and then rebuild the app bundle. ${error}`, + ); + } } return appConfigs; From e9aab60c75aeb3db7b304c6bc17f45ceaf1ddd5a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 Jan 2021 17:03:02 +0100 Subject: [PATCH 49/99] added changesets --- .changeset/loud-walls-collect.md | 5 +++++ .changeset/neat-brooms-allow.md | 5 +++++ .changeset/nice-bottles-battle.md | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 .changeset/loud-walls-collect.md create mode 100644 .changeset/neat-brooms-allow.md create mode 100644 .changeset/nice-bottles-battle.md diff --git a/.changeset/loud-walls-collect.md b/.changeset/loud-walls-collect.md new file mode 100644 index 0000000000..78356eef38 --- /dev/null +++ b/.changeset/loud-walls-collect.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fixed module resolution of external libraries during backend development. Modules used to be resolved relative to the backend entrypoint, but are now resolved relative to each individual module. diff --git a/.changeset/neat-brooms-allow.md b/.changeset/neat-brooms-allow.md new file mode 100644 index 0000000000..6ea45e99ba --- /dev/null +++ b/.changeset/neat-brooms-allow.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Each piece of the configuration schema is now validate upfront, in order to produce more informative errors. diff --git a/.changeset/nice-bottles-battle.md b/.changeset/nice-bottles-battle.md new file mode 100644 index 0000000000..a688241caa --- /dev/null +++ b/.changeset/nice-bottles-battle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app-backend': patch +--- + +Failures to load the frontend configuration schema now throws an error that includes more context and instructions for how to fix the issue. From 81f4b6330a03f7763ef98e1e63a0f8fdcaa5db2e Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Fri, 29 Jan 2021 17:10:03 +0100 Subject: [PATCH 50/99] backend: Gitlab URL Reader to throw error when apiBaseUrl is missing And add more tests for all other URL Readers --- .../src/reading/BitbucketUrlReader.test.ts | 12 ++++++++++++ .../src/reading/GithubUrlReader.test.ts | 15 +++++++++++++++ .../src/reading/GitlabUrlReader.test.ts | 12 ++++++++++++ .../backend-common/src/reading/GitlabUrlReader.ts | 6 ++++++ 4 files changed, 45 insertions(+) diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index 5571a07898..974c84b2c2 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -242,5 +242,17 @@ describe('BitbucketUrlReader', () => { expect(response.etag).toBe('12ab34cd56ef'); }); + + it('should throw error when apiBaseUrl is missing', () => { + expect(() => { + /* eslint-disable no-new */ + new BitbucketUrlReader( + { + host: 'bitbucket.mycompany.net', + }, + { treeResponseFactory }, + ); + }).toThrowError('must configure an explicit apiBaseUrl'); + }); }); }); diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index 975be85ccc..fae3f3ba12 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -376,5 +376,20 @@ describe('GithubUrlReader', () => { }; await expect(fnGithub).rejects.toThrow(NotFoundError); }); + + it('should throw error when apiBaseUrl is missing', () => { + expect(() => { + /* eslint-disable no-new */ + new GithubUrlReader( + { + host: 'ghe.mycompany.net', + }, + { + treeResponseFactory, + credentialsProvider: mockCredentialsProvider, + }, + ); + }).toThrowError('must configure an explicit apiBaseUrl'); + }); }); }); diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index 90acfd1ab9..b18e7a4294 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -379,5 +379,17 @@ describe('GitlabUrlReader', () => { }; await expect(fnGithub).rejects.toThrow(NotFoundError); }); + + it('should throw error when apiBaseUrl is missing', () => { + expect(() => { + /* eslint-disable no-new */ + new GitlabUrlReader( + { + host: 'gitlab.mycompany.com', + }, + { treeResponseFactory }, + ); + }).toThrowError('must configure an explicit apiBaseUrl'); + }); }); }); diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts index 22316f6895..8a763d1259 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -51,6 +51,12 @@ export class GitlabUrlReader implements UrlReader { deps: { treeResponseFactory: ReadTreeResponseFactory }, ) { this.treeResponseFactory = deps.treeResponseFactory; + + if (!config.apiBaseUrl) { + throw new Error( + `GitLab integration for '${config.host}' must configure an explicit apiBaseUrl`, + ); + } } async read(url: string): Promise { From d7f30a800db6e0dcee5de1a08b9ed88428dfe573 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 Jan 2021 17:24:14 +0100 Subject: [PATCH 51/99] Update .changeset/neat-brooms-allow.md Co-authored-by: Himanshu Mishra --- .changeset/neat-brooms-allow.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/neat-brooms-allow.md b/.changeset/neat-brooms-allow.md index 6ea45e99ba..a4c69b0945 100644 --- a/.changeset/neat-brooms-allow.md +++ b/.changeset/neat-brooms-allow.md @@ -2,4 +2,4 @@ '@backstage/config-loader': patch --- -Each piece of the configuration schema is now validate upfront, in order to produce more informative errors. +Each piece of the configuration schema is now validated upfront, in order to produce more informative errors. From a22d297954d6c84a40fffeacf08d849f3dde806d Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Fri, 29 Jan 2021 11:42:52 -0500 Subject: [PATCH 52/99] bump cost insights --- plugins/cost-insights/CHANGELOG.md | 6 ++++++ plugins/cost-insights/package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index a979f1476b..78d4c69da4 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-cost-insights +## 0.7.0 + +### Minor Changes + +- 19172f5a9: add alert hooks + ## 0.6.0 ### Minor Changes diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 72a9e92633..022e778150 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cost-insights", - "version": "0.6.0", + "version": "0.7.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From 357c1f1e5f5d52aead4a75b0cdb29af4b16f7667 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Fri, 29 Jan 2021 14:16:26 -0500 Subject: [PATCH 53/99] Clarify link and Backstage case --- docs/features/kubernetes/installation.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/features/kubernetes/installation.md b/docs/features/kubernetes/installation.md index 9e531de655..35a027df62 100644 --- a/docs/features/kubernetes/installation.md +++ b/docs/features/kubernetes/installation.md @@ -7,8 +7,8 @@ description: Installing Kubernetes plugin into Backstage The Kubernetes feature is a plugin to Backstage, and it is exposed as a tab when viewing entities in the software catalog. -If you haven't setup Backstage already, start -[here](../../getting-started/index.md). +If you haven't setup Backstage already, read the +[Getting Started](../../getting-started/index.md). ## Adding the Kubernetes frontend plugin @@ -111,7 +111,7 @@ Backstage app. ## Running Backstage locally Start the frontend and the backend app by -[running backstage locally](../../getting-started/running-backstage-locally.md). +[running Backstage locally](../../getting-started/running-backstage-locally.md). ## Configuration From e763dcf8c49099068dc819911e980febc90a4ce3 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Fri, 29 Jan 2021 14:17:09 -0500 Subject: [PATCH 54/99] Fix Backstage case --- docs/features/search/README.md | 2 +- .../software-templates/extending/create-your-own-templater.md | 2 +- docs/features/software-templates/index.md | 4 ++-- docs/getting-started/development-environment.md | 2 +- docs/plugins/integrating-plugin-into-service-catalog.md | 2 +- docs/reference/utility-apis/BackstageIdentityApi.md | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/features/search/README.md b/docs/features/search/README.md index 0b13b47979..cd971d9d41 100644 --- a/docs/features/search/README.md +++ b/docs/features/search/README.md @@ -67,7 +67,7 @@ more to come... - As a contributor I should be able to integrate plugin data to the indexing process of Backstage Search by using the standardized API. - As a software engineer I should be able to search for all content (for - example, entities, metadata, documentation) in backstage search. + example, entities, metadata, documentation) in Backstage search. more to come... diff --git a/docs/features/software-templates/extending/create-your-own-templater.md b/docs/features/software-templates/extending/create-your-own-templater.md index 37e68c4153..28769d77e1 100644 --- a/docs/features/software-templates/extending/create-your-own-templater.md +++ b/docs/features/software-templates/extending/create-your-own-templater.md @@ -86,7 +86,7 @@ follows: _note_ Currently the templaters that we provide are basically Docker action containers that are run on top of the skeleton folder. This keeps dependencies -to a minimum for running backstage scaffolder, but you don't _have_ to use +to a minimum for running Backstage scaffolder, but you don't _have_ to use Docker. You can `pip install cookiecutter` to run it locally in your backend. You could create your own templater that spins up an EC2 instance and downloads the folder and does everything using an AMI if you want. It's entirely up to diff --git a/docs/features/software-templates/index.md b/docs/features/software-templates/index.md index 1034ffef0f..898075cb08 100644 --- a/docs/features/software-templates/index.md +++ b/docs/features/software-templates/index.md @@ -39,13 +39,13 @@ internally. ![Enter some variables](../../assets/software-templates/template-picked.png) After filling in these variables, you'll get some more fields to fill out which -are required for backstage usage: the owner (which is a `user` in the backstage +are required for Backstage usage: the owner (which is a `user` in the backstage system), the `storePath` (which right now must be a GitHub Organisation or GitHub user and a non-existing GitHub repository name in the format `organisation/reponame`), and a GitHub team or user account which should be granted admin access to the repository. -![Enter backstage vars](../../assets/software-templates/template-picked-2.png) +![Enter Backstage vars](../../assets/software-templates/template-picked-2.png) ### Run! diff --git a/docs/getting-started/development-environment.md b/docs/getting-started/development-environment.md index b5ff3885b3..01fd7bdc24 100644 --- a/docs/getting-started/development-environment.md +++ b/docs/getting-started/development-environment.md @@ -31,7 +31,7 @@ $ yarn start This should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal. -By default, backstage will start on port 3000, however you can override this by +By default, Backstage will start on port 3000, however you can override this by setting an environment variable `PORT` on your local machine. e.g. `export PORT=8080` then running `yarn start`. Or `PORT=8080 yarn start`. diff --git a/docs/plugins/integrating-plugin-into-service-catalog.md b/docs/plugins/integrating-plugin-into-service-catalog.md index 51d2331140..01de779635 100644 --- a/docs/plugins/integrating-plugin-into-service-catalog.md +++ b/docs/plugins/integrating-plugin-into-service-catalog.md @@ -59,7 +59,7 @@ accordingly inside `Router.tsx`) ### Import and use router in the APP In the `app/src/components/catalog/EntityPage.tsx` (app === your folder, -containing backstage app) import your created Router: +containing Backstage app) import your created Router: ```tsx import { Router as MyPluginRouter } from '@backstage/plugin-my-plugin; diff --git a/docs/reference/utility-apis/BackstageIdentityApi.md b/docs/reference/utility-apis/BackstageIdentityApi.md index dafdb7d55f..69fe5d26ba 100644 --- a/docs/reference/utility-apis/BackstageIdentityApi.md +++ b/docs/reference/utility-apis/BackstageIdentityApi.md @@ -79,7 +79,7 @@ Referenced by: [getBackstageIdentity](#getbackstageidentity).
 export type BackstageIdentity = {
   /**
-   * The backstage user ID.
+   * The Backstage user ID.
    */
   id: string;
 

From baf5615b51c79d6324601f974e10ad51971b2042 Mon Sep 17 00:00:00 2001
From: Adam Harvey 
Date: Fri, 29 Jan 2021 14:17:26 -0500
Subject: [PATCH 55/99] Clarifications

---
 docs/features/techdocs/getting-started.md | 32 +++++++++++------------
 1 file changed, 16 insertions(+), 16 deletions(-)

diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md
index e1ddfd23f9..5147e17aaa 100644
--- a/docs/features/techdocs/getting-started.md
+++ b/docs/features/techdocs/getting-started.md
@@ -158,7 +158,7 @@ techdocs:
   requestUrl: http://localhost:7000/api/techdocs/
 ```
 
-`requestUrl` is used by TechDocs frontend plugin to discover techdocs-backend
+`requestUrl` is used by TechDocs frontend plugin to discover `techdocs-backend`
 endpoints, and the `storageUrl` is another endpoint in `techdocs-backend` which
 acts as a middleware between TechDocs and the storage (where the static
 generated docs site are stored). These default values should mostly work for
@@ -180,8 +180,8 @@ pipeline, and are being stored in a storage somewhere.
 
 When `techdocs.builder` is set to `'external'`, TechDocs becomes more or less a
 read-only experience where it serves static files from a storage containing all
-the generated documentation. Read more in the "Basic" and "Recommended" setup of
-TechDocs [here](architecture.md)
+the generated documentation. Read more in the "Basic" and "Recommended" sections
+of the [TechDocs Architecture](architecture.md).
 
 ### Choosing storage (publisher)
 
@@ -190,9 +190,9 @@ fetch the sites from. This is managed by a
 [Publisher](./concepts.md#techdocs-publisher). Examples: Google Cloud Storage,
 Amazon S3, or local filesystem of Backstage server.
 
-It is okay to use the local filesystem in a "Basic" setup when you are trying
-out Backstage for the first time. Using Cloud Storage is documented
-[here](./using-cloud-storage.md).
+It is okay to use the local filesystem in a "basic" setup when you are trying
+out Backstage for the first time. At a later time, review
+[Using Cloud Storage](./using-cloud-storage.md).
 
 ```yaml
 techdocs:
@@ -229,23 +229,23 @@ environment is compatible with techdocs.
 You will have to install the `mkdocs` and `mkdocs-techdocs-core` package from
 pip, as well as `graphviz` and `plantuml` from your OS package manager (e.g.
 apt). See our
-[Dockerfile](https://github.com/backstage/techdocs-container/blob/main/Dockerfile)
-for the latest requirements. You should be trying to match your Dockerfile with
-this one.
+[`Dockerfile`](https://github.com/backstage/techdocs-container/blob/main/Dockerfile)
+for the latest requirements. You should be trying to match your `Dockerfile`
+with this one.
 
 Note: We recommend Python version 3.7 or higher.
 
-Caveat: Please install the `mkdocs-techdocs-core` package after all other Python
-packages. The order is important to make sure we get correct version of some of
-the dependencies. For example, we want `Markdown` version to be
-[3.2.2](https://github.com/backstage/backstage/blob/f9f70c225548017b6a14daea75b00fbd399c11eb/packages/techdocs-container/techdocs-core/requirements.txt#L11).
-You can also explicitly install `Markdown==3.2.2` after installing all other
-Python packages.
+> Caveat: Please install the `mkdocs-techdocs-core` package after all other
+> Python packages. The order is important to make sure we get correct version of
+> some of the dependencies. For example, we want `Markdown` version to be
+> [3.2.2](https://github.com/backstage/backstage/blob/f9f70c225548017b6a14daea75b00fbd399c11eb/packages/techdocs-container/techdocs-core/requirements.txt#L11).
+> You can also explicitly install `Markdown==3.2.2` after installing all other
+> Python packages.
 
 ## Running Backstage locally
 
 Start the frontend and the backend app by
-[running backstage locally](../../getting-started/running-backstage-locally.md).
+[running Backstage locally](../../getting-started/running-backstage-locally.md).
 
 Open your browser at [http://localhost:3000/docs/](http://localhost:3000/docs/)
 to see all your documentation sites.

From a14d702a7d9ccba4540b60ffa6c1a240e842fb5b Mon Sep 17 00:00:00 2001
From: Adam Harvey 
Date: Fri, 29 Jan 2021 14:20:55 -0500
Subject: [PATCH 56/99] Clarify create plugin step

---
 docs/plugins/create-a-plugin.md | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/docs/plugins/create-a-plugin.md b/docs/plugins/create-a-plugin.md
index bcb86024fd..4ccd48f17e 100644
--- a/docs/plugins/create-a-plugin.md
+++ b/docs/plugins/create-a-plugin.md
@@ -9,8 +9,9 @@ A Backstage Plugin adds functionality to Backstage.
 ## Create a Plugin
 
 To create a new plugin, make sure you've run `yarn install` and installed
-dependencies, then run the following on your command line (invoking the
-`backstage-cli`).
+dependencies, then run the following on your command line (a shortcut to
+invoking the [`backstage-cli create-plugin`](../cli/commands.md#create-plugin))
+from the root of your project.
 
 ```bash
 yarn create-plugin

From 7c0fcd7404d9b5a0171ba1a7426a81d66aad7479 Mon Sep 17 00:00:00 2001
From: blam 
Date: Sat, 30 Jan 2021 01:44:46 +0100
Subject: [PATCH 57/99] chore: added start command to tugboat to wait for the
 port to become ready before marking as live in tugboat

---
 .tugboat/config.yml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.tugboat/config.yml b/.tugboat/config.yml
index 594a1044ac..b8c3c127c8 100644
--- a/.tugboat/config.yml
+++ b/.tugboat/config.yml
@@ -12,3 +12,6 @@ services:
       build:
         - yarn install --frozen-lockfile
         - yarn workspace example-app build
+      start:
+        # wget the endpoint. Will retry every 2 seconds. 30 retries = 1m for service to come up. Plenty.
+        - wget -O /dev/null -o /dev/null --tries=30 --timeout=5 --retry-connrefused http://localhost:7000

From 4c98fbd8c1ec74db1d5849a84c327d0550d5d637 Mon Sep 17 00:00:00 2001
From: Nigel Wright 
Date: Sat, 30 Jan 2021 16:12:35 +1300
Subject: [PATCH 58/99] Fix: defaulted timezone to GMT and added test

---
 .../HomepageTimer/HomepageTimer.test.tsx      | 53 +++++++++++++++++++
 .../layout/HomepageTimer/HomepageTimer.tsx    |  6 +--
 2 files changed, 56 insertions(+), 3 deletions(-)
 create mode 100644 packages/core/src/layout/HomepageTimer/HomepageTimer.test.tsx

diff --git a/packages/core/src/layout/HomepageTimer/HomepageTimer.test.tsx b/packages/core/src/layout/HomepageTimer/HomepageTimer.test.tsx
new file mode 100644
index 0000000000..40b0c266e0
--- /dev/null
+++ b/packages/core/src/layout/HomepageTimer/HomepageTimer.test.tsx
@@ -0,0 +1,53 @@
+/*
+ * 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 { renderWithEffects } from '@backstage/test-utils';
+import { HomepageTimer } from './HomepageTimer';
+import React from 'react';
+import { lightTheme } from '@backstage/theme';
+import { ThemeProvider } from '@material-ui/core';
+
+import {
+  ApiProvider,
+  ApiRegistry,
+  ConfigReader,
+  ConfigApi,
+  configApiRef,
+} from '@backstage/core-api';
+
+it('changes default timezone to GMT', async () => {
+  const configApi: ConfigApi = new ConfigReader({
+    homepage: {
+      clocks: [
+        {
+          label: 'New York',
+          timezone: 'America/New_Pork',
+        },
+      ],
+    },
+    context: 'test',
+  });
+
+  const rendered = await renderWithEffects(
+    
+      
+        
+      
+    ,
+  );
+
+  expect(rendered.getByText('GMT')).toBeInTheDocument();
+});
diff --git a/packages/core/src/layout/HomepageTimer/HomepageTimer.tsx b/packages/core/src/layout/HomepageTimer/HomepageTimer.tsx
index 219665599c..2adbf8fbd5 100644
--- a/packages/core/src/layout/HomepageTimer/HomepageTimer.tsx
+++ b/packages/core/src/layout/HomepageTimer/HomepageTimer.tsx
@@ -51,10 +51,10 @@ function getTimes(configApi: ConfigApi) {
       } catch (e) {
         // eslint-disable-next-line no-console
         console.warn(
-          `The timezone ${options.timeZone} is invalid. Defaulting to America/Los Angeles`,
+          `The timezone ${options.timeZone} is invalid. Defaulting to GMT`,
         );
-        options.timeZone = 'America/Los_Angeles';
-        label = 'Los Angeles';
+        options.timeZone = 'GMT';
+        label = 'GMT';
       }
 
       const time = d.toLocaleTimeString(lang, options);

From 23d81ecc1b38c42af0154dcfc7ea85bcf81a3511 Mon Sep 17 00:00:00 2001
From: Patrik Oldsberg 
Date: Sat, 30 Jan 2021 16:24:01 +0100
Subject: [PATCH 59/99] app: fix cost-insights dependency

---
 packages/app/package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/app/package.json b/packages/app/package.json
index 36fe7651e6..bcf048c693 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -12,7 +12,7 @@
     "@backstage/plugin-catalog-import": "^0.3.6",
     "@backstage/plugin-circleci": "^0.2.6",
     "@backstage/plugin-cloudbuild": "^0.2.7",
-    "@backstage/plugin-cost-insights": "^0.6.0",
+    "@backstage/plugin-cost-insights": "^0.7.0",
     "@backstage/plugin-explore": "^0.2.3",
     "@backstage/plugin-gcp-projects": "^0.2.3",
     "@backstage/plugin-github-actions": "^0.3.0",

From 4408806374936b1b1e616066157198488963e0d1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= 
Date: Sat, 30 Jan 2021 20:26:35 +0100
Subject: [PATCH 60/99] make the master workflows manually triggerable

---
 .github/workflows/master-win.yml | 1 +
 .github/workflows/master.yml     | 1 +
 2 files changed, 2 insertions(+)

diff --git a/.github/workflows/master-win.yml b/.github/workflows/master-win.yml
index ca402c48d5..e6a9bf158b 100644
--- a/.github/workflows/master-win.yml
+++ b/.github/workflows/master-win.yml
@@ -1,6 +1,7 @@
 name: Master Build Windows
 
 on:
+  workflow_dispatch:
   push:
     branches: [master]
 
diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml
index 68b08224ac..0a615b7c82 100644
--- a/.github/workflows/master.yml
+++ b/.github/workflows/master.yml
@@ -1,6 +1,7 @@
 name: Main Master Build
 
 on:
+  workflow_dispatch:
   push:
     branches: [master]
 

From 15fffa5fff5c1d1da17f9398c4a7cd60f35874bb Mon Sep 17 00:00:00 2001
From: Himanshu Mishra 
Date: Sun, 31 Jan 2021 16:07:21 +0100
Subject: [PATCH 61/99] chore: sync yarn.lock after TechDocs Azure Storage PR

Follow up of https://github.com/backstage/backstage/pull/4017
---
 yarn.lock | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 245 insertions(+), 8 deletions(-)

diff --git a/yarn.lock b/yarn.lock
index c40b1ab675..75291bd66c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -954,6 +954,101 @@
   dependencies:
     tslib "^1.8.0"
 
+"@azure/abort-controller@^1.0.0":
+  version "1.0.2"
+  resolved "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.2.tgz#822405c966b2aec16fb62c1b19d37eaccf231995"
+  integrity sha512-XUyTo+bcyxHEf+jlN2MXA7YU9nxVehaubngHV1MIZZaqYmZqykkoeAz/JMMEeR7t3TcyDwbFa3Zw8BZywmIx4g==
+  dependencies:
+    tslib "^2.0.0"
+
+"@azure/core-asynciterator-polyfill@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.npmjs.org/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.0.tgz#dcccebb88406e5c76e0e1d52e8cc4c43a68b3ee7"
+  integrity sha512-kmv8CGrPfN9SwMwrkiBK9VTQYxdFQEGe0BmQk+M8io56P9KNzpAxcWE/1fxJj7uouwN4kXF0BHW8DNlgx+wtCg==
+
+"@azure/core-auth@^1.1.3":
+  version "1.1.4"
+  resolved "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.1.4.tgz#af9a334acf3cb9c49e6013e6caf6dc9d43476030"
+  integrity sha512-+j1embyH1jqf04AIfJPdLafd5SC1y6z1Jz4i+USR1XkTp6KM8P5u4/AjmWMVoEQdM/M29PJcRDZcCEWjK9S1bw==
+  dependencies:
+    "@azure/abort-controller" "^1.0.0"
+    tslib "^2.0.0"
+
+"@azure/core-http@^1.2.0":
+  version "1.2.2"
+  resolved "https://registry.npmjs.org/@azure/core-http/-/core-http-1.2.2.tgz#a6f7717184fd2657d3acabd1d64dfdc0bd531ce3"
+  integrity sha512-9eu2OcbR7e44gqBy4U1Uv8NTWgLIMwKXMEGgO2MahsJy5rdTiAhs5fJHQffPq8uX2MFh21iBODwO9R/Xlov88A==
+  dependencies:
+    "@azure/abort-controller" "^1.0.0"
+    "@azure/core-auth" "^1.1.3"
+    "@azure/core-tracing" "1.0.0-preview.9"
+    "@azure/logger" "^1.0.0"
+    "@opentelemetry/api" "^0.10.2"
+    "@types/node-fetch" "^2.5.0"
+    "@types/tunnel" "^0.0.1"
+    form-data "^3.0.0"
+    node-fetch "^2.6.0"
+    process "^0.11.10"
+    tough-cookie "^4.0.0"
+    tslib "^2.0.0"
+    tunnel "^0.0.6"
+    uuid "^8.3.0"
+    xml2js "^0.4.19"
+
+"@azure/core-lro@^1.0.2":
+  version "1.0.3"
+  resolved "https://registry.npmjs.org/@azure/core-lro/-/core-lro-1.0.3.tgz#1ddfb4ecdb81ce87b5f5d972ffe2acbbc46e524e"
+  integrity sha512-Py2crJ84qx1rXkzIwfKw5Ni4WJuzVU7KAF6i1yP3ce8fbynUeu8eEWS4JGtSQgU7xv02G55iPDROifmSDbxeHA==
+  dependencies:
+    "@azure/abort-controller" "^1.0.0"
+    "@azure/core-http" "^1.2.0"
+    events "^3.0.0"
+    tslib "^2.0.0"
+
+"@azure/core-paging@^1.1.1":
+  version "1.1.3"
+  resolved "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.1.3.tgz#3587c9898a0530cacb64bab216d7318468aa5efc"
+  integrity sha512-his7Ah40ThEYORSpIAwuh6B8wkGwO/zG7gqVtmSE4WAJ46e36zUDXTKReUCLBDc6HmjjApQQxxcRFy5FruG79A==
+  dependencies:
+    "@azure/core-asynciterator-polyfill" "^1.0.0"
+
+"@azure/core-tracing@1.0.0-preview.9":
+  version "1.0.0-preview.9"
+  resolved "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.9.tgz#84f3b85572013f9d9b85e1e5d89787aa180787eb"
+  integrity sha512-zczolCLJ5QG42AEPQ+Qg9SRYNUyB+yZ5dzof4YEc+dyWczO9G2sBqbAjLB7IqrsdHN2apkiB2oXeDKCsq48jug==
+  dependencies:
+    "@opencensus/web-types" "0.0.7"
+    "@opentelemetry/api" "^0.10.2"
+    tslib "^2.0.0"
+
+"@azure/identity@^1.2.2":
+  version "1.2.2"
+  resolved "https://registry.npmjs.org/@azure/identity/-/identity-1.2.2.tgz#00d673c8881778c55777fcc76e822b42466d3fc7"
+  integrity sha512-aYkeNXl52aEHW1iOZQJb3SC7Vvbu87f01iNT+pSVHwj09LpN9+gP/Lb9uoWy36Fgv9WlukM55LbjLSbb1Renqw==
+  dependencies:
+    "@azure/core-http" "^1.2.0"
+    "@azure/core-tracing" "1.0.0-preview.9"
+    "@azure/logger" "^1.0.0"
+    "@azure/msal-node" "1.0.0-beta.3"
+    "@opentelemetry/api" "^0.10.2"
+    axios "^0.21.1"
+    events "^3.0.0"
+    jws "^4.0.0"
+    msal "^1.0.2"
+    open "^7.0.0"
+    qs "^6.7.0"
+    tslib "^2.0.0"
+    uuid "^8.3.0"
+  optionalDependencies:
+    keytar "^5.4.0"
+
+"@azure/logger@^1.0.0":
+  version "1.0.1"
+  resolved "https://registry.npmjs.org/@azure/logger/-/logger-1.0.1.tgz#19b333203d1b2931353d8879e814b64a7274837a"
+  integrity sha512-QYQeaJ+A5x6aMNu8BG5qdsVBnYBop9UMwgUvGihSjf1PdZZXB+c/oMdM2ajKwzobLBh9e9QuMQkN9iL+IxLBLA==
+  dependencies:
+    tslib "^2.0.0"
+
 "@azure/msal-common@^2.1.0":
   version "2.1.0"
   resolved "https://registry.npmjs.org/@azure/msal-common/-/msal-common-2.1.0.tgz#a4bc17e254d6ec524016f13267947dd4ff4a624d"
@@ -961,7 +1056,7 @@
   dependencies:
     debug "^4.1.1"
 
-"@azure/msal-node@^1.0.0-beta.3":
+"@azure/msal-node@1.0.0-beta.3", "@azure/msal-node@^1.0.0-beta.3":
   version "1.0.0-beta.3"
   resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.3.tgz#c84c7948028b39e48b901f5fac35bdedcbc8772e"
   integrity sha512-/KfYRfrsOIrZONvo/0Vi5umuqbPBtCWNtmRvkse64uI0C4CP/W4WXwRD42VMws/8LtKvr1I5rYlYgFzt5zDz/A==
@@ -971,6 +1066,21 @@
     jsonwebtoken "^8.5.1"
     uuid "^8.3.0"
 
+"@azure/storage-blob@^12.4.0":
+  version "12.4.0"
+  resolved "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.4.0.tgz#7127ddd9f413105e2c3688691bc4c6245d0806b3"
+  integrity sha512-OnhVSoKD1HzBB79/rFzPbC4w9TdzFXeoOwkX+aIu3rb8qvN0VaqvUqZXSrBCyG2LcLyVkY4MPCJQBrmEUm9kvw==
+  dependencies:
+    "@azure/abort-controller" "^1.0.0"
+    "@azure/core-http" "^1.2.0"
+    "@azure/core-lro" "^1.0.2"
+    "@azure/core-paging" "^1.1.1"
+    "@azure/core-tracing" "1.0.0-preview.9"
+    "@azure/logger" "^1.0.0"
+    "@opentelemetry/api" "^0.10.2"
+    events "^3.0.0"
+    tslib "^2.0.0"
+
 "@babel/code-frame@7.0.0":
   version "7.0.0"
   resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
@@ -4917,6 +5027,23 @@
     fast-deep-equal "^3.1.3"
     lodash.clonedeep "^4.5.0"
 
+"@opencensus/web-types@0.0.7":
+  version "0.0.7"
+  resolved "https://registry.npmjs.org/@opencensus/web-types/-/web-types-0.0.7.tgz#4426de1fe5aa8f624db395d2152b902874f0570a"
+  integrity sha512-xB+w7ZDAu3YBzqH44rCmG9/RlrOmFuDPt/bpf17eJr8eZSrLt7nc7LnWdxM9Mmoj/YKMHpxRg28txu3TcpiL+g==
+
+"@opentelemetry/api@^0.10.2":
+  version "0.10.2"
+  resolved "https://registry.npmjs.org/@opentelemetry/api/-/api-0.10.2.tgz#9647b881f3e1654089ff7ea59d587b2d35060654"
+  integrity sha512-GtpMGd6vkzDMYcpu2t9LlhEgMy/SzBwRnz48EejlRArYqZzqSzAsKmegUK7zHgl+EOIaK9mKHhnRaQu3qw20cA==
+  dependencies:
+    "@opentelemetry/context-base" "^0.10.2"
+
+"@opentelemetry/context-base@^0.10.2":
+  version "0.10.2"
+  resolved "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.10.2.tgz#55bea904b2b91aa8a8675df9eaba5961bddb1def"
+  integrity sha512-hZNKjKOYsckoOEgBziGMnBcX0M7EtstnCmwz5jZUOUYwlZ+/xxX6z3jPu1XVO2Jivk0eLfuP9GP+vFD49CMetw==
+
 "@panva/asn1.js@^1.0.0":
   version "1.0.0"
   resolved "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz#dd55ae7b8129e02049f009408b97c61ccf9032f6"
@@ -6949,6 +7076,14 @@
     "@types/node" "*"
     form-data "^3.0.0"
 
+"@types/node-fetch@^2.5.0":
+  version "2.5.8"
+  resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.8.tgz#e199c835d234c7eb0846f6618012e558544ee2fb"
+  integrity sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw==
+  dependencies:
+    "@types/node" "*"
+    form-data "^3.0.0"
+
 "@types/node@*", "@types/node@>= 8":
   version "14.0.26"
   resolved "https://registry.npmjs.org/@types/node/-/node-14.0.26.tgz#22a3b8a46510da8944b67bfc27df02c34a35331c"
@@ -7408,6 +7543,13 @@
   resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d"
   integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==
 
+"@types/tunnel@^0.0.1":
+  version "0.0.1"
+  resolved "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.1.tgz#0d72774768b73df26f25df9184273a42da72b19c"
+  integrity sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A==
+  dependencies:
+    "@types/node" "*"
+
 "@types/uglify-js@*":
   version "3.0.4"
   resolved "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082"
@@ -11882,7 +12024,7 @@ detect-indent@^6.0.0:
   resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-6.0.0.tgz#0abd0f549f69fc6659a254fe96786186b6f528fd"
   integrity sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==
 
-detect-libc@^1.0.2:
+detect-libc@^1.0.2, detect-libc@^1.0.3:
   version "1.0.3"
   resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
   integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
@@ -13125,6 +13267,11 @@ expand-brackets@^2.1.4:
     snapdragon "^0.8.1"
     to-regex "^3.0.1"
 
+expand-template@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
+  integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
+
 expand-tilde@^2.0.0, expand-tilde@^2.0.2:
   version "2.0.2"
   resolved "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
@@ -14207,6 +14354,11 @@ gitconfiglocal@^1.0.0:
   dependencies:
     ini "^1.3.2"
 
+github-from-package@0.0.0:
+  version "0.0.0"
+  resolved "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
+  integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=
+
 github-slugger@^1.3.0:
   version "1.3.0"
   resolved "https://registry.npmjs.org/github-slugger/-/github-slugger-1.3.0.tgz#9bd0a95c5efdfc46005e82a906ef8e2a059124c9"
@@ -17253,6 +17405,14 @@ kafkajs@^1.16.0-beta.6:
   resolved "https://registry.npmjs.org/kafkajs/-/kafkajs-1.16.0-beta.6.tgz#650f4d16abd60516aa0c375be613368391216df1"
   integrity sha512-R4DT3s7oCAoxdq3tN8w2WkUxLXcZgLQSjQVSmEZpEmT6hVdZ5AUZWqyLYr1IRbQowsJK4Z7eWJAk68J0JiRlUw==
 
+keytar@^5.4.0:
+  version "5.6.0"
+  resolved "https://registry.npmjs.org/keytar/-/keytar-5.6.0.tgz#7b5d4bd043d17211163640be6c4a27a49b12bb39"
+  integrity sha512-ueulhshHSGoryfRXaIvTj0BV1yB0KddBGhGoqCxSN9LR1Ks1GKuuCdVhF+2/YOs5fMl6MlTI9On1a4DHDXoTow==
+  dependencies:
+    nan "2.14.1"
+    prebuild-install "5.3.3"
+
 keyv@^3.0.0:
   version "3.1.0"
   resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
@@ -18822,6 +18982,13 @@ ms@2.1.2, ms@^2.0.0, ms@^2.1.1:
   resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
+msal@^1.0.2:
+  version "1.4.4"
+  resolved "https://registry.npmjs.org/msal/-/msal-1.4.4.tgz#3f9b5a4442aa711c12ab8e88b8ed89b293f99711"
+  integrity sha512-aOBD/L6jAsizDFzKxxvXxH0FEDjp6Inr3Ufi/Y2o7KCFKN+akoE2sLeszEb/0Y3VxHxK0F0ea7xQ/HHTomKivw==
+  dependencies:
+    tslib "^1.9.3"
+
 msw@^0.20.5:
   version "0.20.5"
   resolved "https://registry.npmjs.org/msw/-/msw-0.20.5.tgz#b6141080c0d8b17c451d9ca36c28cc47b4ac487a"
@@ -18926,7 +19093,7 @@ mz@^2.5.0, mz@^2.7.0:
     object-assign "^4.0.1"
     thenify-all "^1.0.0"
 
-nan@^2.12.1, nan@^2.14.0:
+nan@2.14.1, nan@^2.12.1, nan@^2.14.0:
   version "2.14.1"
   resolved "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
   integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
@@ -18967,6 +19134,11 @@ nanomatch@^1.2.9:
     snapdragon "^0.8.1"
     to-regex "^3.0.1"
 
+napi-build-utils@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
+  integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
+
 native-url@^0.2.6:
   version "0.2.6"
   resolved "https://registry.npmjs.org/native-url/-/native-url-0.2.6.tgz#ca1258f5ace169c716ff44eccbddb674e10399ae"
@@ -19041,6 +19213,13 @@ nock@^13.0.5:
     lodash.set "^4.3.2"
     propagate "^2.0.0"
 
+node-abi@^2.7.0:
+  version "2.19.3"
+  resolved "https://registry.npmjs.org/node-abi/-/node-abi-2.19.3.tgz#252f5dcab12dad1b5503b2d27eddd4733930282d"
+  integrity sha512-9xZrlyfvKhWme2EXFKQhZRp1yNWT/uI1luYPr3sFl+H4keYY4xR+1jO7mvTTijIsHf1M+QDe9uWuKeEpLInIlg==
+  dependencies:
+    semver "^5.4.1"
+
 node-addon-api@2.0.0:
   version "2.0.0"
   resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz#f9afb8d777a91525244b01775ea0ddbe1125483b"
@@ -19235,6 +19414,11 @@ nodemon@^2.0.2:
     undefsafe "^2.0.3"
     update-notifier "^4.1.0"
 
+noop-logger@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2"
+  integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=
+
 "nopt@2 || 3":
   version "3.0.6"
   resolved "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
@@ -19372,7 +19556,7 @@ npm-run-path@^4.0.0:
   dependencies:
     path-key "^3.0.0"
 
-"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.2, npmlog@^4.1.2:
+"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.1, npmlog@^4.0.2, npmlog@^4.1.2:
   version "4.1.2"
   resolved "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
   integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
@@ -19620,6 +19804,14 @@ onetime@^5.1.0:
   dependencies:
     mimic-fn "^2.1.0"
 
+open@^7.0.0:
+  version "7.3.1"
+  resolved "https://registry.npmjs.org/open/-/open-7.3.1.tgz#111119cb919ca1acd988f49685c4fdd0f4755356"
+  integrity sha512-f2wt9DCBKKjlFbjzGb8MOAW8LH8F0mrs1zc7KTjAJ9PZNQbfenzWbNP1VZJvw6ICMG9r14Ah6yfwPn7T7i646A==
+  dependencies:
+    is-docker "^2.0.0"
+    is-wsl "^2.1.1"
+
 open@^7.0.2, open@^7.0.3:
   version "7.2.1"
   resolved "https://registry.npmjs.org/open/-/open-7.2.1.tgz#07b0ade11a43f2a8ce718480bdf3d7563a095195"
@@ -21044,6 +21236,27 @@ postgres-interval@^1.1.0:
   dependencies:
     xtend "^4.0.0"
 
+prebuild-install@5.3.3:
+  version "5.3.3"
+  resolved "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.3.tgz#ef4052baac60d465f5ba6bf003c9c1de79b9da8e"
+  integrity sha512-GV+nsUXuPW2p8Zy7SarF/2W/oiK8bFQgJcncoJ0d7kRpekEA0ftChjfEaF9/Y+QJEc/wFR7RAEa8lYByuUIe2g==
+  dependencies:
+    detect-libc "^1.0.3"
+    expand-template "^2.0.3"
+    github-from-package "0.0.0"
+    minimist "^1.2.0"
+    mkdirp "^0.5.1"
+    napi-build-utils "^1.0.1"
+    node-abi "^2.7.0"
+    noop-logger "^0.1.1"
+    npmlog "^4.0.1"
+    pump "^3.0.0"
+    rc "^1.2.7"
+    simple-get "^3.0.3"
+    tar-fs "^2.0.0"
+    tunnel-agent "^0.6.0"
+    which-pm-runs "^1.0.0"
+
 precond@0.2:
   version "0.2.3"
   resolved "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac"
@@ -21311,7 +21524,7 @@ pseudomap@^1.0.2:
   resolved "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
   integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
 
-psl@^1.1.28:
+psl@^1.1.28, psl@^1.1.33:
   version "1.8.0"
   resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
   integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
@@ -21404,6 +21617,11 @@ qs@^6.5.1, qs@^6.6.0, qs@^6.9.1, qs@^6.9.4:
   resolved "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687"
   integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==
 
+qs@^6.7.0:
+  version "6.9.6"
+  resolved "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee"
+  integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==
+
 qs@~6.5.2:
   version "6.5.2"
   resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
@@ -24558,6 +24776,16 @@ tapable@^1.0.0, tapable@^1.1.3:
   resolved "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
   integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
 
+tar-fs@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
+  integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
+  dependencies:
+    chownr "^1.1.1"
+    mkdirp-classic "^0.5.2"
+    pump "^3.0.0"
+    tar-stream "^2.1.4"
+
 tar-fs@~2.0.1:
   version "2.0.1"
   resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz#e44086c1c60d31a4f0cf893b1c4e155dabfae9e2"
@@ -24993,6 +25221,15 @@ tough-cookie@^3.0.1:
     psl "^1.1.28"
     punycode "^2.1.1"
 
+tough-cookie@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4"
+  integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==
+  dependencies:
+    psl "^1.1.33"
+    punycode "^2.1.1"
+    universalify "^0.1.2"
+
 tr46@^1.0.1:
   version "1.0.1"
   resolved "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
@@ -25200,7 +25437,7 @@ tunnel-agent@^0.6.0:
   dependencies:
     safe-buffer "^5.0.1"
 
-tunnel@0.0.6:
+tunnel@0.0.6, tunnel@^0.0.6:
   version "0.0.6"
   resolved "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
   integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
@@ -25513,7 +25750,7 @@ universal-user-agent@^6.0.0:
   resolved "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee"
   integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==
 
-universalify@^0.1.0:
+universalify@^0.1.0, universalify@^0.1.2:
   version "0.1.2"
   resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
   integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
@@ -26460,7 +26697,7 @@ xml-name-validator@^3.0.0:
   resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
   integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
 
-xml2js@^0.4.23:
+xml2js@^0.4.19, xml2js@^0.4.23:
   version "0.4.23"
   resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
   integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==

From 398e1f83ee3391eedda533c10859e7a769670da1 Mon Sep 17 00:00:00 2001
From: Patrik Oldsberg 
Date: Wed, 16 Dec 2020 22:41:41 +0100
Subject: [PATCH 62/99] cli: update frontend plugin template to use new
 extension API

---
 .changeset/cyan-kiwis-suffer.md               |  5 +++++
 .../commands/create-plugin/createPlugin.ts    | 13 +++++------
 .../templates/default-plugin/dev/index.tsx    |  4 ----
 .../default-plugin/dev/index.tsx.hbs          | 11 ++++++++++
 .../ExampleComponent.test.tsx.hbs             | 13 ++++++-----
 .../ExampleComponent/ExampleComponent.tsx.hbs |  6 ++---
 .../src/components/ExampleComponent/index.ts  |  2 +-
 .../ExampleFetchComponent.test.tsx.hbs        | 12 ++++++----
 .../ExampleFetchComponent.tsx.hbs             |  6 ++---
 .../components/ExampleFetchComponent/index.ts |  2 +-
 .../cli/templates/default-plugin/src/index.ts |  1 -
 .../templates/default-plugin/src/index.ts.hbs |  1 +
 .../default-plugin/src/plugin.test.ts.hbs     |  4 ++--
 .../default-plugin/src/plugin.ts.hbs          | 22 +++++++++++--------
 .../default-plugin/src/routes.ts.hbs          |  5 +++++
 15 files changed, 64 insertions(+), 43 deletions(-)
 create mode 100644 .changeset/cyan-kiwis-suffer.md
 delete mode 100644 packages/cli/templates/default-plugin/dev/index.tsx
 create mode 100644 packages/cli/templates/default-plugin/dev/index.tsx.hbs
 delete mode 100644 packages/cli/templates/default-plugin/src/index.ts
 create mode 100644 packages/cli/templates/default-plugin/src/index.ts.hbs
 create mode 100644 packages/cli/templates/default-plugin/src/routes.ts.hbs

diff --git a/.changeset/cyan-kiwis-suffer.md b/.changeset/cyan-kiwis-suffer.md
new file mode 100644
index 0000000000..b59b0e9a32
--- /dev/null
+++ b/.changeset/cyan-kiwis-suffer.md
@@ -0,0 +1,5 @@
+---
+'@backstage/cli': patch
+---
+
+Update `create-plugin` template to use the new composability API, by switching to exporting a single routable extension component.
diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts
index a2005c5580..c9f1664411 100644
--- a/packages/cli/src/commands/create-plugin/createPlugin.ts
+++ b/packages/cli/src/commands/create-plugin/createPlugin.ts
@@ -20,6 +20,7 @@ import chalk from 'chalk';
 import inquirer, { Answers, Question } from 'inquirer';
 import { exec as execCb } from 'child_process';
 import { resolve as resolvePath, join as joinPath } from 'path';
+import camelCase from 'lodash/camelCase';
 import os from 'os';
 import { Command } from 'commander';
 import {
@@ -106,14 +107,10 @@ export async function addPluginDependencyToApp(
 
 export async function addPluginToApp(
   rootDir: string,
-  pluginName: string,
+  pluginVar: string,
   pluginPackage: string,
 ) {
-  const pluginNameCapitalized = pluginName
-    .split('-')
-    .map(name => capitalize(name))
-    .join('');
-  const pluginExport = `export { plugin as ${pluginNameCapitalized} } from '${pluginPackage}';`;
+  const pluginExport = `export { ${pluginVar} } from '${pluginPackage}';`;
   const pluginsFilePath = 'packages/app/src/plugins.ts';
   const pluginsFile = resolvePath(rootDir, pluginsFilePath);
 
@@ -223,6 +220,7 @@ export default async (cmd: Command) => {
   const name = cmd.scope
     ? `@${cmd.scope.replace(/^@/, '')}/plugin-${pluginId}`
     : `plugin-${pluginId}`;
+  const pluginVar = `${camelCase(answers.id)}Plugin`;
   const npmRegistry = cmd.npmRegistry && cmd.scope ? cmd.npmRegistry : '';
   const privatePackage = cmd.private === false ? false : true;
   const isMonoRepo = await fs.pathExists(paths.resolveTargetRoot('lerna.json'));
@@ -259,6 +257,7 @@ export default async (cmd: Command) => {
       tempDir,
       {
         ...answers,
+        pluginVar,
         pluginVersion,
         name,
         privatePackage,
@@ -278,7 +277,7 @@ export default async (cmd: Command) => {
       await addPluginDependencyToApp(paths.targetRoot, name, pluginVersion);
 
       Task.section('Import plugin in app');
-      await addPluginToApp(paths.targetRoot, pluginId, name);
+      await addPluginToApp(paths.targetRoot, pluginVar, name);
     }
 
     if (ownerIds && ownerIds.length) {
diff --git a/packages/cli/templates/default-plugin/dev/index.tsx b/packages/cli/templates/default-plugin/dev/index.tsx
deleted file mode 100644
index 6fce113093..0000000000
--- a/packages/cli/templates/default-plugin/dev/index.tsx
+++ /dev/null
@@ -1,4 +0,0 @@
-import { createDevApp } from '@backstage/dev-utils';
-import { plugin } from '../src/plugin';
-
-createDevApp().registerPlugin(plugin).render();
diff --git a/packages/cli/templates/default-plugin/dev/index.tsx.hbs b/packages/cli/templates/default-plugin/dev/index.tsx.hbs
new file mode 100644
index 0000000000..13705ecd58
--- /dev/null
+++ b/packages/cli/templates/default-plugin/dev/index.tsx.hbs
@@ -0,0 +1,11 @@
+import React from 'react';
+import { createDevApp } from '@backstage/dev-utils';
+import { {{ pluginVar }}, ExamplePage } from '../src/plugin';
+
+createDevApp()
+  .registerPlugin({{ pluginVar }})
+  .addPage({
+    element: ,
+    title: 'Root Page',
+  })
+  .render();
diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs
index e805900f36..e6eab17493 100644
--- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs
+++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs
@@ -1,13 +1,12 @@
 import React from 'react';
 import { render } from '@testing-library/react';
-import ExampleComponent from './ExampleComponent';
+import { ExampleComponent } from './ExampleComponent';
 import { ThemeProvider } from '@material-ui/core';
 import { lightTheme } from '@backstage/theme';
 import { rest } from 'msw';
 import { setupServer } from 'msw/node';
 import { msw } from '@backstage/test-utils';
 
-
 describe('ExampleComponent', () => {
   const server = setupServer();
   // Enable sane handlers for network requests
@@ -15,15 +14,17 @@ describe('ExampleComponent', () => {
 
   // setup mock response
   beforeEach(() => {
-    server.use(rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))))
-  })
+    server.use(
+      rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))),
+    );
+  });
 
   it('should render', () => {
     const rendered = render(
       
         
       ,
-      );
-      expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument();
+    );
+    expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument();
   });
 });
diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs
index 5f90f2de1e..dcecebdf46 100644
--- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs
+++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs
@@ -9,9 +9,9 @@ import {
   HeaderLabel,
   SupportButton,
 } from '@backstage/core';
-import ExampleFetchComponent from '../ExampleFetchComponent';
+import { ExampleFetchComponent } from '../ExampleFetchComponent';
 
-const ExampleComponent = () => (
+export const ExampleComponent = () => (
   
     
@@ -36,5 +36,3 @@ const ExampleComponent = () => ( ); - -export default ExampleComponent; diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/index.ts b/packages/cli/templates/default-plugin/src/components/ExampleComponent/index.ts index 520a3bf553..8b8437521b 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/index.ts +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/index.ts @@ -1 +1 @@ -export { default } from './ExampleComponent'; +export { ExampleComponent } from './ExampleComponent'; diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs index 81e1b4be09..fa1e289d8e 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs @@ -1,6 +1,6 @@ import React from 'react'; import { render } from '@testing-library/react'; -import ExampleFetchComponent from './ExampleFetchComponent'; +import { ExampleFetchComponent } from './ExampleFetchComponent'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { msw } from '@backstage/test-utils'; @@ -9,11 +9,15 @@ describe('ExampleFetchComponent', () => { const server = setupServer(); // Enable sane handlers for network requests msw.setupDefaultHandlers(server); - + // setup mock response beforeEach(() => { - server.use(rest.get('https://randomuser.me/*', (_, res, ctx) => res(ctx.status(200), ctx.delay(2000), ctx.json({})))) - }) + server.use( + rest.get('https://randomuser.me/*', (_, res, ctx) => + res(ctx.status(200), ctx.delay(2000), ctx.json({})), + ), + ); + }); it('should render', async () => { const rendered = render(); expect(await rendered.findByTestId('progress')).toBeInTheDocument(); diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs index 8cc5ed2ab7..20dd6d1f57 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs @@ -48,7 +48,7 @@ export const DenseTable = ({ users }: DenseTableProps) => { { title: 'Nationality', field: 'nationality' }, ]; - const data = users.map((user) => { + const data = users.map(user => { return { avatar: ( { ); }; -const ExampleFetchComponent = () => { +export const ExampleFetchComponent = () => { const { value, loading, error } = useAsync(async (): Promise => { const response = await fetch('https://randomuser.me/api/?results=20'); const data = await response.json(); @@ -88,5 +88,3 @@ const ExampleFetchComponent = () => { return ; }; - -export default ExampleFetchComponent; diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/index.ts b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/index.ts index 3e53453948..41a43e84f1 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/index.ts +++ b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/index.ts @@ -1 +1 @@ -export { default } from './ExampleFetchComponent'; +export { ExampleFetchComponent } from './ExampleFetchComponent'; diff --git a/packages/cli/templates/default-plugin/src/index.ts b/packages/cli/templates/default-plugin/src/index.ts deleted file mode 100644 index 99edba26c3..0000000000 --- a/packages/cli/templates/default-plugin/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { plugin } from './plugin'; diff --git a/packages/cli/templates/default-plugin/src/index.ts.hbs b/packages/cli/templates/default-plugin/src/index.ts.hbs new file mode 100644 index 0000000000..165c8419e1 --- /dev/null +++ b/packages/cli/templates/default-plugin/src/index.ts.hbs @@ -0,0 +1 @@ +export { {{ pluginVar }} } from './plugin'; diff --git a/packages/cli/templates/default-plugin/src/plugin.test.ts.hbs b/packages/cli/templates/default-plugin/src/plugin.test.ts.hbs index c2be0a5301..9d44a9c497 100644 --- a/packages/cli/templates/default-plugin/src/plugin.test.ts.hbs +++ b/packages/cli/templates/default-plugin/src/plugin.test.ts.hbs @@ -1,7 +1,7 @@ -import { plugin } from './plugin'; +import { {{ pluginVar }} } from './plugin'; describe('{{ id }}', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect({{ pluginVar }}).toBeDefined(); }); }); diff --git a/packages/cli/templates/default-plugin/src/plugin.ts.hbs b/packages/cli/templates/default-plugin/src/plugin.ts.hbs index 1bf4d07cdb..2a836592f7 100644 --- a/packages/cli/templates/default-plugin/src/plugin.ts.hbs +++ b/packages/cli/templates/default-plugin/src/plugin.ts.hbs @@ -1,14 +1,18 @@ -import { createPlugin, createRouteRef } from '@backstage/core'; -import ExampleComponent from './components/ExampleComponent'; +import { createPlugin, createRoutableExtension } from '@backstage/core'; -export const rootRouteRef = createRouteRef({ - path: '/{{ id }}', - title: '{{ id }}', -}); +import { rootRouteRef } from './routes'; -export const plugin = createPlugin({ +export const {{ pluginVar }} = createPlugin({ id: '{{ id }}', - register({ router }) { - router.addRoute(rootRouteRef, ExampleComponent); + routes: { + root: rootRouteRef, }, }); + +export const ExamplePage = {{ pluginVar }}.provide( + createRoutableExtension({ + component: () => + import('./components/ExampleComponent').then(m => m.ExampleComponent), + mountPoint: rootRouteRef, + }), +); diff --git a/packages/cli/templates/default-plugin/src/routes.ts.hbs b/packages/cli/templates/default-plugin/src/routes.ts.hbs new file mode 100644 index 0000000000..b2afba074d --- /dev/null +++ b/packages/cli/templates/default-plugin/src/routes.ts.hbs @@ -0,0 +1,5 @@ +import { createRouteRef } from '@backstage/core'; + +export const rootRouteRef = createRouteRef({ + title: '{{ id }}', +}); From 8a8882ce06773b20522888a8c78f9381781e2f3a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 Jan 2021 00:34:47 +0100 Subject: [PATCH 63/99] cli: update plugin:diff to not require src/routes.ts --- packages/cli/src/commands/plugin/diff.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/cli/src/commands/plugin/diff.ts b/packages/cli/src/commands/plugin/diff.ts index 416d67869b..fb96250567 100644 --- a/packages/cli/src/commands/plugin/diff.ts +++ b/packages/cli/src/commands/plugin/diff.ts @@ -39,6 +39,11 @@ const fileHandlers = [ patterns: ['package.json'], handler: handlers.packageJson, }, + { + // Not all plugins have routes + patterns: ['src/routes.ts'], + handler: handlers.skip, + }, { // make sure files in 1st level of src/ and dev/ exist patterns: ['.eslintrc.js', /^(src|dev)\/[^/]+$/], From fe3211cf3852caacd8d030d2ce10c1ad31b11042 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 31 Jan 2021 15:12:29 +0100 Subject: [PATCH 64/99] cli: make create-plugin add extension to app if possible --- .../commands/create-plugin/createPlugin.ts | 48 ++++++++++++++++++- .../default-plugin/dev/index.tsx.hbs | 4 +- .../templates/default-plugin/src/index.ts.hbs | 2 +- .../default-plugin/src/plugin.ts.hbs | 2 +- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index c9f1664411..affbf64922 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -21,6 +21,7 @@ import inquirer, { Answers, Question } from 'inquirer'; import { exec as execCb } from 'child_process'; import { resolve as resolvePath, join as joinPath } from 'path'; import camelCase from 'lodash/camelCase'; +import upperFirst from 'lodash/upperFirst'; import os from 'os'; import { Command } from 'commander'; import { @@ -105,7 +106,7 @@ export async function addPluginDependencyToApp( }); } -export async function addPluginToApp( +export async function addPluginImportToApp( rootDir: string, pluginVar: string, pluginPackage: string, @@ -123,6 +124,46 @@ export async function addPluginToApp( }); } +export async function addPluginExtensionToApp( + pluginId: string, + extensionName: string, + pluginPackage: string, +) { + const pluginsFilePath = paths.resolveTargetRoot('packages/app/src/App.tsx'); + if (!(await fs.pathExists(pluginsFilePath))) { + return; + } + + await Task.forItem('processing', pluginsFilePath, async () => { + const content = await fs.readFile(pluginsFilePath, 'utf8'); + const revLines = content.split('\n').reverse(); + + const lastImportIndex = revLines.findIndex(line => + line.match(/ from ("|').*("|')/), + ); + const lastRouteIndex = revLines.findIndex(line => + line.match(/<\/FlatRoutes/), + ); + + if (lastImportIndex !== -1 && lastRouteIndex !== -1) { + revLines.splice( + lastImportIndex, + 0, + `import { ${extensionName} } from '${pluginPackage}';`, + ); + const [indentation] = revLines[lastRouteIndex + 1].match(/^\s*/) ?? []; + revLines.splice( + lastRouteIndex + 1, + 0, + `${indentation}}/>`, + ); + + const newContent = revLines.reverse().join('\n'); + await fs.writeFile(pluginsFilePath, newContent, 'utf8'); + } + }); +} + async function cleanUp(tempDir: string) { await Task.forItem('remove', 'temporary directory', async () => { await fs.remove(tempDir); @@ -221,6 +262,7 @@ export default async (cmd: Command) => { ? `@${cmd.scope.replace(/^@/, '')}/plugin-${pluginId}` : `plugin-${pluginId}`; const pluginVar = `${camelCase(answers.id)}Plugin`; + const extensionName = `${upperFirst(camelCase(answers.id))}Page`; const npmRegistry = cmd.npmRegistry && cmd.scope ? cmd.npmRegistry : ''; const privatePackage = cmd.private === false ? false : true; const isMonoRepo = await fs.pathExists(paths.resolveTargetRoot('lerna.json')); @@ -259,6 +301,7 @@ export default async (cmd: Command) => { ...answers, pluginVar, pluginVersion, + extensionName, name, privatePackage, npmRegistry, @@ -277,7 +320,8 @@ export default async (cmd: Command) => { await addPluginDependencyToApp(paths.targetRoot, name, pluginVersion); Task.section('Import plugin in app'); - await addPluginToApp(paths.targetRoot, pluginVar, name); + await addPluginImportToApp(paths.targetRoot, pluginVar, name); + await addPluginExtensionToApp(pluginId, extensionName, name); } if (ownerIds && ownerIds.length) { diff --git a/packages/cli/templates/default-plugin/dev/index.tsx.hbs b/packages/cli/templates/default-plugin/dev/index.tsx.hbs index 13705ecd58..ade00a1613 100644 --- a/packages/cli/templates/default-plugin/dev/index.tsx.hbs +++ b/packages/cli/templates/default-plugin/dev/index.tsx.hbs @@ -1,11 +1,11 @@ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; -import { {{ pluginVar }}, ExamplePage } from '../src/plugin'; +import { {{ pluginVar }}, {{ extensionName }} } from '../src/plugin'; createDevApp() .registerPlugin({{ pluginVar }}) .addPage({ - element: , + element: <{{ extensionName }} />, title: 'Root Page', }) .render(); diff --git a/packages/cli/templates/default-plugin/src/index.ts.hbs b/packages/cli/templates/default-plugin/src/index.ts.hbs index 165c8419e1..be4881efaf 100644 --- a/packages/cli/templates/default-plugin/src/index.ts.hbs +++ b/packages/cli/templates/default-plugin/src/index.ts.hbs @@ -1 +1 @@ -export { {{ pluginVar }} } from './plugin'; +export { {{ pluginVar }}, {{ extensionName }} } from './plugin'; diff --git a/packages/cli/templates/default-plugin/src/plugin.ts.hbs b/packages/cli/templates/default-plugin/src/plugin.ts.hbs index 2a836592f7..0ae5356fd1 100644 --- a/packages/cli/templates/default-plugin/src/plugin.ts.hbs +++ b/packages/cli/templates/default-plugin/src/plugin.ts.hbs @@ -9,7 +9,7 @@ export const {{ pluginVar }} = createPlugin({ }, }); -export const ExamplePage = {{ pluginVar }}.provide( +export const {{ extensionName }} = {{ pluginVar }}.provide( createRoutableExtension({ component: () => import('./components/ExampleComponent').then(m => m.ExampleComponent), From ceef4dd897879a9456ae1bcfe85cb583f02795f1 Mon Sep 17 00:00:00 2001 From: Ioannis Georgoulas Date: Sun, 31 Jan 2021 18:43:28 +0000 Subject: [PATCH 65/99] [Catalog Import] Export APIs --- .changeset/catalog-import-export-api-snow-fight.md | 5 +++++ plugins/catalog-import/src/index.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/catalog-import-export-api-snow-fight.md diff --git a/.changeset/catalog-import-export-api-snow-fight.md b/.changeset/catalog-import-export-api-snow-fight.md new file mode 100644 index 0000000000..dce9811f10 --- /dev/null +++ b/.changeset/catalog-import-export-api-snow-fight.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-import': patch +--- + +Export _api_ (Client, API, ref) from the catalog import plugin. diff --git a/plugins/catalog-import/src/index.ts b/plugins/catalog-import/src/index.ts index ff7857cacd..dd8ca2bbb4 100644 --- a/plugins/catalog-import/src/index.ts +++ b/plugins/catalog-import/src/index.ts @@ -16,3 +16,4 @@ export { plugin } from './plugin'; export { Router } from './components/Router'; +export * from './api'; From a26668913ccc9c31fc7c5655309bd7c667d6ee65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sat, 30 Jan 2021 20:15:09 +0100 Subject: [PATCH 66/99] scaffolder-backend: attempt to fix windows test errors in master --- .changeset/little-cherries-hug.md | 5 +++ plugins/scaffolder-backend/package.json | 2 ++ .../src/scaffolder/jobs/processor.test.ts | 32 ++++++++++++++++--- .../src/scaffolder/jobs/processor.ts | 6 ++-- .../src/scaffolder/stages/prepare/file.ts | 3 +- .../scaffolder/stages/prepare/github.test.ts | 2 +- .../stages/templater/cookiecutter.test.ts | 4 +-- .../stages/templater/cookiecutter.ts | 4 +-- 8 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 .changeset/little-cherries-hug.md diff --git a/.changeset/little-cherries-hug.md b/.changeset/little-cherries-hug.md new file mode 100644 index 0000000000..5fb42cbdad --- /dev/null +++ b/.changeset/little-cherries-hug.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Attempt to fix windows test errors in master diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 57b62be2cd..7af9f567e3 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -62,7 +62,9 @@ "@backstage/cli": "^0.5.0", "@backstage/test-utils": "^0.1.5", "@types/fs-extra": "^9.0.1", + "@types/mock-fs": "^4.13.0", "@types/supertest": "^2.0.8", + "mock-fs": "^4.13.0", "supertest": "^4.0.2", "yaml": "^1.10.0", "msw": "^0.21.2" diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts index 5ff2bd1640..c782037ec8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import os from 'os'; -import { JobProcessor } from './processor'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { StageInput } from './types'; -import { RequiredTemplateValues } from '../stages/templater'; import parseGitUrl from 'git-url-parse'; +import mockFs from 'mock-fs'; +import os from 'os'; +import { RequiredTemplateValues } from '../stages/templater'; +import { makeLogStream } from './logger'; +import { JobProcessor } from './processor'; +import { StageInput } from './types'; describe('JobProcessor', () => { const mockEntity: TemplateEntityV1alpha1 = { @@ -32,7 +34,7 @@ describe('JobProcessor', () => { name: 'graphql-starter', title: 'GraphQL Service', description: - 'A GraphQL starter template for backstage to get you up and running\nthe best pracices with GraphQL\n', + 'A GraphQL starter template for backstage to get you up and running\nthe best practices with GraphQL\n', uid: '9cf16bad-16e0-4213-b314-c4eec773c50b', etag: 'ZTkxMjUxMjUtYWY3Yi00MjU2LWFkYWMtZTZjNjU5ZjJhOWM2', @@ -71,6 +73,25 @@ describe('JobProcessor', () => { const workingDirectory = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; + // NOTE(freben): Without this line, mock-fs makes winston/logform break. + // There are a number of reported issues with logform and its use of dynamic + // strings for imports. It confuses webpack. The basic fix is to trigger + // those imports before mock-fs runs. I wanted to add a mock dir + // 'node_modules': mockFs.passthrough(), but that doesn't seem to be a thing + // in mock-fs 4. + // Probable REAL fix: https://github.com/winstonjs/logform/pull/117 + makeLogStream({}); + + beforeEach(() => { + mockFs({ + [workingDirectory]: mockFs.directory(), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + describe('create', () => { it('creates should create a new job with a unique id', async () => { const processor = new JobProcessor(workingDirectory); @@ -157,6 +178,7 @@ describe('JobProcessor', () => { expect(processor.get(job.id)).toBe(job); }); }); + describe('process', () => { it('throws an error when the status of the job is not in pending state', async () => { const processor = new JobProcessor(workingDirectory); diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts index 08423872f2..425fc07946 100644 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts +++ b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts @@ -131,7 +131,7 @@ export class JobProcessor implements Processor { try { for (const stage of job.stages) { - // Create a logger for each stage so we can create seperate + // Create a logger for each stage so we can create separate // Streams for each step. const { logger, log, stream } = makeLogStream({ id: job.id, @@ -151,8 +151,8 @@ export class JobProcessor implements Processor { logStream: stream, }); - // If the handler returns something, then let's merge this onto the ontext - // For the next stage to use as it might be relevant. + // If the handler returns something, then let's merge this onto the + // context for the next stage to use as it might be relevant. if (handlerResponse) { job.context = { ...job.context, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts index cbf81bdb06..4d49ba222e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts @@ -15,6 +15,7 @@ */ import fs from 'fs-extra'; import path from 'path'; +import { fileURLToPath } from 'url'; import { InputError } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; @@ -27,7 +28,7 @@ export class FilePreparer implements PreparerBase { const checkoutDir = path.join(workspacePath, 'checkout'); await fs.ensureDir(checkoutDir); - const templatePath = url.slice('file://'.length); + const templatePath = fileURLToPath(url); await fs.copy(templatePath, checkoutDir, { recursive: true, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index a5fad5eaaa..301f015c1e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -60,7 +60,7 @@ describe('GitHubPreparer', () => { path.resolve(checkoutPath, 'templates', 'graphql-starter', 'template'), templatePath, ); - expect(fs.rmdir).toHaveBeenCalledWith('/tmp/template/.git'); + expect(fs.rmdir).toHaveBeenCalledWith(path.resolve(templatePath, '.git')); }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts index 8172986d69..1a3ad76499 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts @@ -227,8 +227,8 @@ describe('CookieCutter Templater', () => { args: expect.arrayContaining([ '--no-input', '-o', - 'tempdir/intermediate', - 'tempdir/template', + path.join('tempdir', 'intermediate'), + path.join('tempdir', 'template'), '--verbose', ]), logStream: stream, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts index d19112561a..09d4faa37c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts @@ -26,7 +26,7 @@ export class CookieCutter implements TemplaterBase { directory: string, ): Promise> { try { - return await fs.readJSON(`${directory}/cookiecutter.json`); + return await fs.readJSON(path.join(directory, 'cookiecutter.json')); } catch (ex) { if (ex.code !== 'ENOENT') { throw ex; @@ -54,7 +54,7 @@ export class CookieCutter implements TemplaterBase { ...values, }; - await fs.writeJSON(`${templateDir}/cookiecutter.json`, cookieInfo); + await fs.writeJSON(path.join(templateDir, 'cookiecutter.json'), cookieInfo); const cookieCutterInstalled = await commandExists('cookiecutter'); if (cookieCutterInstalled) { From 55b298c52a1b8a9c1c16cae929b864835a270de7 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Sun, 31 Jan 2021 12:30:01 +0100 Subject: [PATCH 67/99] scaffolder: Fix test on windows; incorrect file URL --- .../src/scaffolder/stages/prepare/file.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts index 36be4d705b..0c94edce42 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts @@ -26,16 +26,17 @@ describe('File preparer', () => { it('prepares templates from a file path', async () => { const logger = getVoidLogger(); const preparer = new FilePreparer(); - const workspacePath = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; + const root = os.platform() === 'win32' ? 'C:\\' : '/'; + const workspacePath = path.join(root, 'tmp'); const checkoutPath = path.resolve(workspacePath, 'checkout'); await preparer.prepare({ - url: 'file:///path/to/template', + url: `file:///${root}path/to/template`, logger, workspacePath, }); expect(fs.copy).toHaveBeenCalledWith( - path.join('/path', 'to', 'template'), + path.join(root, 'path', 'to', 'template'), checkoutPath, { recursive: true, From 58a28f0b87752e769aaf545a88f1ec1e25fca8ad Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 31 Jan 2021 20:01:54 +0100 Subject: [PATCH 68/99] docs: update plugin create and reference docs for top-level routable extensions --- docs/architecture.drawio | 1 + docs/plugins/plugin-development.md | 67 ++++++++++++++++++--------- docs/plugins/structure-of-a-plugin.md | 40 +++++++++------- docs/reference/createPlugin-router.md | 31 ------------- docs/reference/createPlugin.md | 14 ++---- 5 files changed, 74 insertions(+), 79 deletions(-) create mode 100644 docs/architecture.drawio delete mode 100644 docs/reference/createPlugin-router.md diff --git a/docs/architecture.drawio b/docs/architecture.drawio new file mode 100644 index 0000000000..dbe2a5f6fa --- /dev/null +++ b/docs/architecture.drawio @@ -0,0 +1 @@ +7VpLc6M4EP41PiaFBLbh6LxmLlubSqZ2Z+amBQVUBYgScmzPrx9hJIMQGJzEhtTOxUaNnp++bnU3mtm3yfYLQ1n0Fw1wPINWsJ3ZdzMIAfQ88VdIdqXEs2EpCBkJZKVK8Ex+YSm0pHRNApxrFTmlMSeZLvRpmmKfazLEGN3o1V5orI+aoRAbgmcfxab0XxLwqJS6cFnJv2ISRmpksJALTpCqLFeSRyigm5rIvp/Zt4xSXj4l21scF+ApXMp2Dx1vDxNjOOVDGvwk7Nv6n4erTfLNgzcr9/HvH5sru+zlFcVrueBVlsn58p0CId+QJEapKN3I+phxvO2cCDgsT/AC0wRzthNVZANXAiIZARayvKnwPdSJathCRwqR3NPw0HW1bPEgV34CCs4gFBhdpwEu+rEEEJuIcPycIb94uxHkF7KIJ2LcOzAUp+4t6QQP6uA5JnZt0J0NOduEzsAtFMBlnXhIxUX/qerWqXxyFv2EsuFFCQVNvbrfcpzmhKb5aMRyjsPo6CgCZ1xqKSbVMHyia46f8MtEEQSuDqEzNoLgsyPYRkLQpsrnw9CA8IHRVKhyIKSP8Tok6buPy7cgBVtsnHVRG2eSqwOOC1ELfqZz05ucZh6HDyygjh8cFz8wPdPWB+C0COga+I3voPRosM5AAMamoNUN4UQRbPjJYGQOgoWBoAGcOGhXRRgvSn6M8pz4OlYByqM9rhVwODBC+iGwiWHpmvl4wJZzxEJ8rMOOgKUG9LwFaCVjOEacvOqLaENfjvBIiVjeYZu9xjZbjf0rlykbwVrCQPWjGtqNhuWyjYb7vT+s8h10WL6fDmKf2e67VLN94UdRuIZzVb7b1t/e7VRpS/h31Yl4rjcTxapVUVCNzs63oXQblW9LnW/eqXRTNs7R+3EvzD7zRJy6R9F/IF42VgKmV1uciCxF8dSh7D8ZLxx2ms7F6baw1aQdt2iFLXuWQ1DGIxpSsXv3lbTTwvYZ2MnYSm9MW3lg1k63ne81lmA+zFoKwqBdrVpWVMiPzLcxzsI6bVq2Vl88lBP4UMsNB+Sga2px+A5j1d3HooBiEqaFNglGYiYEhaUhPopX8kVCgmCvBKKPrOg52YbFZ6/r8ksTLP+LbhXr9x/CgH0t8L1JKfcjKX+JSfb1zFbtqunvt+SMnBaqw7MZtXnr+TDpoLORj3THTumaIdPkIWy4Ke7IcTs0w4xVRibrmzSV2DWV+LLwfb7MEVhMTYvbHeVpYzgxNbZND1mo8QPyOZW9TxDDiemybWbRDeSGRhkqlBgSZczOHAsobvQGA+VZMFqizr2eL4ED3fJ3sdTY8eY8SiMfA5r0OXMiRV3w+rCs7ttj0jdm/ZafI+tnW7MRydtkWTNqGErXnuzhR8WxDTdq2RPHNr0u7wJxrG1eGjIU508cW23GaHEs+N9uVNellJ1uWj5+Y2bFtRt1U7ZUuOq+sX3/Gw== \ No newline at end of file diff --git a/docs/plugins/plugin-development.md b/docs/plugins/plugin-development.md index 109f6ed6ba..bfeed58c8f 100644 --- a/docs/plugins/plugin-development.md +++ b/docs/plugins/plugin-development.md @@ -23,35 +23,60 @@ browser APIs or by depending on external modules to do the work. ### Routing -Each plugin is responsible for registering its components to corresponding -routes in the app. +Each plugin can export routable extensions, which are then imported into the app +and mounted at a path. -The app will call the `createPlugin` method on each plugin, passing in a -`router` object with a set of methods on it. +First you will need a `RouteRef` instance to serve as the mount point of your +extensions. This can be used within your own plugin to create a link to the +extension page using `useRouteRef`, as well as for other plugins to link to your +extension. -```jsx +It is best to place these in a separate top-level `src/routes.ts` file, in order +to avoid import cycles, for example like this: + +```tsx +/* src/routes.ts */ +import { createRouteRef } from '@backstage/core'; + +// Note: This route ref is for internal use only, don't export it from the plugin +export const rootRouteRef = createRouteRef({ + title: 'Example Page', +}); +``` + +Now that we have a `RouteRef`, we import it into `src/plugin.ts`, create our +plugin instance with `createPlugin`, as well as create and wrap our routable +extension using `createRoutableExtension` from `@backstage/core`: + +```tsx +/* src/plugin.ts */ import { createPlugin, createRouteRef } from '@backstage/core'; import ExampleComponent from './components/ExampleComponent'; -export const rootRouteRef = createRouteRef({ - path: '/new-plugin', - title: 'New plugin', -}); - -export const plugin = createPlugin({ - id: 'new-plugin', - register({ router }) { - router.addRoute(rootRouteRef, ExampleComponent); +// Create a plugin instance and export this from your plugin package +export const examplePlugin = createPlugin({ + id: 'example', + routes: { + root: rootRouteRef, // This is where the route ref should be exported for usage in the app }, }); + +// This creates a routable extension, which are typically full pages of content. +// Each extension should also be exported from your plugin package. +export const ExamplePage = examplePlugin.provide( + createRoutableExtension({ + // The component needs to be lazy-loaded. It's what will actually be rendered in the end. + component: () => + import('./components/ExampleComponent').then(m => m.ExampleComponent), + // This binds the extension to this route ref, which allows for routing within and across plugin extensions + mountPoint: rootRouteRef, + }), +); ``` -#### `router` API +This extension can then be imported and used in the app as follow, typically +placed within the top-level ``: -```typescript -addRoute( - target: RouteRef, - Component: ComponentType, - options?: RouteOptions, -): void; +```tsx +} /> ``` diff --git a/docs/plugins/structure-of-a-plugin.md b/docs/plugins/structure-of-a-plugin.md index 3f631a9a46..2aacd02cf2 100644 --- a/docs/plugins/structure-of-a-plugin.md +++ b/docs/plugins/structure-of-a-plugin.md @@ -28,6 +28,7 @@ new-plugin/ index.ts plugin.test.ts plugin.ts + routes.ts jest.config.js jest.setup.ts package.json @@ -56,26 +57,30 @@ package.json to declare the plugin dependencies, metadata and scripts. In the `src` folder we get to the interesting bits. Check out the `plugin.ts`: ```jsx -import { createPlugin, createRouteRef } from '@backstage/core'; -import ExampleComponent from './components/ExampleComponent'; +import { createPlugin, createRoutableExtension } from '@backstage/core'; -export const rootRouteRef = createRouteRef({ - path: '/new-plugin', - title: 'New plugin', -}); +import { rootRouteRef } from './routes'; -export const plugin = createPlugin({ - id: 'new-plugin', - register({ router }) { - router.addRoute(rootRouteRef, ExampleComponent); +export const examplePlugin = createPlugin({ + id: 'example', + routes: { + root: rootRouteRef, }, }); + +export const ExamplePage = examplePlugin.provide( + createRoutableExtension({ + component: () => + import('./components/ExampleComponent').then(m => m.ExampleComponent), + mountPoint: rootRouteRef, + }), +); ``` -This is where the plugin is created and where it hooks into the app by declaring -what component should be shown on what URL. See reference docs for -[createPlugin](../reference/createPlugin.md) or -[router](../reference/createPlugin-router.md). +This is where the plugin is created and where it creates and exports extensions +that can be imported and used the app. See reference docs for +[createPlugin](../reference/createPlugin.md) or introduction to the new +[Composability System](./composability.md). ## Components @@ -91,12 +96,15 @@ You may tweak these components, rename them and/or replace them completely. ## Connecting the plugin to the Backstage app -There are two things needed for a Backstage app to start making use of a plugin. +There are three things needed for a Backstage app to start making use of a +plugin. 1. Add plugin as dependency in `app/package.json` 2. `import` plugin in `app/src/plugins.ts` +3. Import and use one or more plugin extensions, for example in + `app/src/App.tsx`. -Luckily these two steps happen automatically when you create a plugin with the +Luckily these three steps happen automatically when you create a plugin with the Backstage CLI. ## Talking to the outside world diff --git a/docs/reference/createPlugin-router.md b/docs/reference/createPlugin-router.md deleted file mode 100644 index 0ef5bdbd0f..0000000000 --- a/docs/reference/createPlugin-router.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -id: createPlugin-router -title: createPlugin - router -description: Documentation on createPlugin - router ---- - -The router that is passed to the `register` function makes it possible for -plugins to hook into routing of the Backstage app and provide the end users with -new views to navigate to. This is done by utilising the following methods on the -`router`: - -```typescript -addRoute( - target: RouteRef, - Component: ComponentType, - options?: RouteOptions, -): void; -``` - -## RouteRef - -`addRoute` method is using mutable RouteRefs, which can be created as following: - -```ts -import { createRouteRef } from '@backstage/core'; - -const myPluginRouteRef = createRouteRef({ - path: '/my-plugin', - title: 'My Plugin', -}); -``` diff --git a/docs/reference/createPlugin.md b/docs/reference/createPlugin.md index 4a58a5ecdc..e3df66fa12 100644 --- a/docs/reference/createPlugin.md +++ b/docs/reference/createPlugin.md @@ -17,32 +17,24 @@ type PluginConfig = { }; type PluginHooks = { - router: RouterHooks; + featureFlags: FeatureFlagsHooks; }; ``` -- [Read more about the router here](createPlugin-router.md) - [Read more about feature flags here](createPlugin-feature-flags.md) ## Example Uses ### Creating a basic plugin -Showcasing adding a route and a feature flag. +Showcasing adding a feature flag. ```jsx -import { createPlugin, createRouteRef } from '@backstage/core'; -import ExampleComponent from './components/ExampleComponent'; - -export const rootRouteRef = createRouteRef({ - path: '/new-plugin', - title: 'New Plugin', -}); +import { createPlugin } from '@backstage/core'; export default createPlugin({ id: 'new-plugin', register({ router, featureFlags }) { - router.addRoute(rootRouteRef, ExampleComponent); featureFlags.register('enable-example-component'); }, }); From 3fa63a36984f79080c765f784b6cbc29273924cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Feb 2021 05:04:15 +0000 Subject: [PATCH 69/99] chore(deps): bump @rollup/plugin-commonjs from 16.0.0 to 17.1.0 Bumps [@rollup/plugin-commonjs](https://github.com/rollup/plugins) from 16.0.0 to 17.1.0. - [Release notes](https://github.com/rollup/plugins/releases) - [Commits](https://github.com/rollup/plugins/compare/commonjs-v16.0.0...commonjs-v17.1.0) Signed-off-by: dependabot[bot] --- packages/cli/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 35ad049741..ed373f6058 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -35,7 +35,7 @@ "@lerna/package-graph": "^3.18.5", "@lerna/project": "^3.18.0", "@octokit/request": "^5.4.12", - "@rollup/plugin-commonjs": "^16.0.0", + "@rollup/plugin-commonjs": "^17.1.0", "@rollup/plugin-json": "^4.0.2", "@rollup/plugin-node-resolve": "^9.0.0", "@rollup/plugin-yaml": "^2.1.1", diff --git a/yarn.lock b/yarn.lock index 75291bd66c..2ac7dffa6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5234,10 +5234,10 @@ react-router-dom "6.0.0-beta.0" react-use "^15.3.3" -"@rollup/plugin-commonjs@^16.0.0": - version "16.0.0" - resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-16.0.0.tgz#169004d56cd0f0a1d0f35915d31a036b0efe281f" - integrity sha512-LuNyypCP3msCGVQJ7ki8PqYdpjfEkE/xtFa5DqlF+7IBD0JsfMZ87C58heSwIMint58sAUZbt3ITqOmdQv/dXw== +"@rollup/plugin-commonjs@^17.1.0": + version "17.1.0" + resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz#757ec88737dffa8aa913eb392fade2e45aef2a2d" + integrity sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew== dependencies: "@rollup/pluginutils" "^3.1.0" commondir "^1.0.1" From 24e47ef1e3b1d84ea7ef0cf8f24fd9477374c22b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 1 Feb 2021 10:17:42 +0100 Subject: [PATCH 70/99] catalog-backend: throw correct error for not allowed entities --- .changeset/poor-ligers-flow.md | 5 +++++ plugins/catalog-backend/src/ingestion/LocationReaders.ts | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/poor-ligers-flow.md diff --git a/.changeset/poor-ligers-flow.md b/.changeset/poor-ligers-flow.md new file mode 100644 index 0000000000..1c3a8a742b --- /dev/null +++ b/.changeset/poor-ligers-flow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Throw `NotAllowedError` when registering locations with entities of disallowed kinds diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index 046b6bdf76..a789e669d0 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; +import { NotAllowedError, UrlReader } from '@backstage/backend-common'; import { Entity, EntityPolicy, @@ -102,7 +102,7 @@ export class LocationReaders implements LocationReader { } else { output.errors.push({ location: item.location, - error: new Error( + error: new NotAllowedError( `Entity of kind ${item.entity.kind} is not allowed from location ${item.location.type} ${item.location.target}`, ), }); From 978d4ab5a25cae186f4a08b43c2eb6bdd362ca7d Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 1 Feb 2021 10:54:53 +0100 Subject: [PATCH 71/99] docs(software-templates): updating docs for storePath --- docs/features/software-templates/index.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/features/software-templates/index.md b/docs/features/software-templates/index.md index 898075cb08..026c3f4bfb 100644 --- a/docs/features/software-templates/index.md +++ b/docs/features/software-templates/index.md @@ -40,10 +40,9 @@ internally. After filling in these variables, you'll get some more fields to fill out which are required for Backstage usage: the owner (which is a `user` in the backstage -system), the `storePath` (which right now must be a GitHub Organisation or -GitHub user and a non-existing GitHub repository name in the format -`organisation/reponame`), and a GitHub team or user account which should be -granted admin access to the repository. +system), and the `storePath` which is a destination URL to create for the +provider for instance http://github.com/backstage/my-new-repository, or +https://gitlab.com/myorg/myrepo. ![Enter Backstage vars](../../assets/software-templates/template-picked-2.png) From b32e65fd519e787eb53c6d85cd4f8d30fb46bf0a Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 1 Feb 2021 11:15:28 +0100 Subject: [PATCH 72/99] Only reexport CatalogApi from catalog-client --- plugins/catalog-react/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-react/src/index.ts b/plugins/catalog-react/src/index.ts index b0a9743ea4..9b6e13bb26 100644 --- a/plugins/catalog-react/src/index.ts +++ b/plugins/catalog-react/src/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from '@backstage/catalog-client'; +export type { CatalogApi } from '@backstage/catalog-client'; export { catalogApiRef } from './api'; export * from './components'; export * from './hooks'; From 59828b177f0952d0602a7cb62400b028a2cf724e Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 1 Feb 2021 11:27:31 +0100 Subject: [PATCH 73/99] Add catalog-client as dev dependency as it's used in tests --- plugins/catalog-import/package.json | 1 + .../catalog-import/src/components/ImportComponentForm.test.tsx | 3 ++- .../catalog-import/src/components/ImportComponentPage.test.tsx | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 4c2017cda7..7f3cc3d1e3 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -49,6 +49,7 @@ "yaml": "^1.10.0" }, "devDependencies": { + "@backstage/catalog-client": "^0.3.5", "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", diff --git a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx index 344dd9d470..38b1065325 100644 --- a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx +++ b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { CatalogClient } from '@backstage/catalog-client'; import { ApiProvider, ApiRegistry, DiscoveryApi, errorApiRef, } from '@backstage/core'; -import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import React from 'react'; diff --git a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx index ad9abbe797..f6ef07e1b2 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { CatalogClient } from '@backstage/catalog-client'; import { ApiProvider, ApiRegistry, configApiRef, errorApiRef, } from '@backstage/core'; -import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { msw, renderInTestApp } from '@backstage/test-utils'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import { rest } from 'msw'; From f27b40e6affaf26344c5a256ff72470f6a91a427 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 1 Feb 2021 11:42:23 +0100 Subject: [PATCH 74/99] docs: TechDocs HOW TO migrate from basic to recommended approach --- docs/features/techdocs/architecture.md | 3 ++- docs/features/techdocs/how-to-guides.md | 30 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/features/techdocs/architecture.md b/docs/features/techdocs/architecture.md index 92cd78b071..b69672246a 100644 --- a/docs/features/techdocs/architecture.md +++ b/docs/features/techdocs/architecture.md @@ -12,7 +12,8 @@ out-of-the box experience. TechDocs Architecture diagram > Note: See below for our recommended deployment architecture which takes care -> of stability, scalability and speed. +> of stability, scalability and speed. Also look at the +> [HOW TO migrate guide](how-to-guides.md#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach). When you open a TechDocs site in Backstage, the [TechDocs Reader](./concepts.md#techdocs-reader) makes a request to diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index b32ff40589..2cb769b0e3 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -5,6 +5,36 @@ sidebar_label: "HOW TO" guides description: TechDocs "HOW TO" guides related to TechDocs --- +## How to migrate from TechDocs Basic to Recommended deployment approach? + +The main difference between TechDocs Basic and Recommended deployment approach +is where the docs are generated and stored. In Basic or the out-of-the-box +setup, docs are generated and stored at the server running your Backstage +instance. But the recommended setup is to generate docs on CI/CD and store the +generated sites to an external storage (e.g. AWS S3 or GCS). TechDocs in your +Backstage instance should turn into read-only mode. Read more details and the +benefits in the [TechDocs Architecture](architecture.md). + +Here are the steps needed to switch from the Basic to Recommended setup - + +### 1. Prepare a cloud storage + +Choose a cloud storage provider like AWS, Google Cloud or Microsoft Azure. +Follow the detailed instructions for +[using cloud storage](using-cloud-storage.md) in TechDocs. + +### 2. Publish to storage from CI/CD + +Start publishing your TechDocs sites from the CI/CD workflow of each repository +containing the source markdown files. Read the detailed instructions for +[configuring CI/CD](configuring-ci-cd.md). + +### 3. Switch TechDocs to read-only mode + +In your Backstage instance's `app-config.yaml`, set `techdocs.builder` from +`'local'` to `'external'`. By doing this, TechDocs will not try to generate +docs. Look at [TechDocs configuration](configuration.md) for reference. + ## How to use URL Reader in TechDocs Prepare step? If TechDocs is configured to generate docs, it will first download the From ae508116d9a6530cfa0cd191463cecd6638cf11b Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 1 Feb 2021 12:02:44 +0100 Subject: [PATCH 75/99] docs: TechDocs revamp techdocs architecture page Remove/Update features which have been implemented. --- docs/features/techdocs/architecture.md | 80 ++++++++++---------------- 1 file changed, 29 insertions(+), 51 deletions(-) diff --git a/docs/features/techdocs/architecture.md b/docs/features/techdocs/architecture.md index b69672246a..8d25b4047f 100644 --- a/docs/features/techdocs/architecture.md +++ b/docs/features/techdocs/architecture.md @@ -17,28 +17,26 @@ out-of-the box experience. When you open a TechDocs site in Backstage, the [TechDocs Reader](./concepts.md#techdocs-reader) makes a request to -`techdocs-backend` with the entity ID and the path of the current page you are -looking at. In response, it receives the static files (HTML, CSS, JSON, etc.) to -render on the page in TechDocs/Backstage. +`techdocs-backend` plugin with the entity ID and the path of the current page +you are looking at. In response, it receives the static files (HTML, CSS, JSON, +etc.) to render on the page in TechDocs/Backstage. The static files consist of HTML, CSS and Images generated by MkDocs. We remove all the JavaScript before adding them to Backstage for security reasons. And -there are some additional techdocs metadata JSON files that TechDocs needs to -render a site. +there is an additional `techdocs_metadata.json` file that TechDocs needs to +render a site. It's important that you use either +[techdocs-cli](https://github.com/backstage/techdocs-cli) or +[techdocs-container](https://github.com/backstage/techdocs-container) to +generate the docs for the expected output. The TechDocs Reader then applies a list of "Transformers" (see [Concepts](./concepts.md)) which modify the generated static HTML files for a number of use cases e.g. Remove certain headers, filter out some HTML tags, etc. Currently, we use the Backstage server's (or techdocs-backend's) local file -system to store the generated files. Publishing to an external storage system -(AWS S3, GCS, etc.) is also possible, but has not been implemented yet. - -A word about `UrlReader` vs Git preparer - Right now, we have two ways to fetch -files from its source repository for docs site generation. 1. By using Git -and 2. By directly using Source control (GitHub, Azure, etc.) APIs. This work is -heavily in progress. Please reach out to us on Discord in the #docs-like-code -channel to talk about it. +system to store the generated files. However, it's ideal to use an external +storage system (e.g. AWS S3, GCS or Azure Blob Storage). Read more in +[using cloud storage](using-cloud-storage.md). ## Recommended deployment @@ -50,22 +48,16 @@ The key difference in the recommended deployment approach is where the docs are built. We assume each entity lives in a repository somewhere (GitHub, GitLab, etc.). We -recommend using a CI/CD pipeline with the repository that has a dedicated -step/job to generate docs for TechDocs. The generated static files are then -stored in a cloud storage solution of your choice. -[Track progress here](https://github.com/backstage/backstage/issues/3096). +recommend [using a CI/CD pipeline](configuring-ci-cd.md) with the repository +that has a dedicated step/job to generate docs for TechDocs. The generated +static files are then [stored in a cloud storage](using-cloud-storage.md) +solution of your choice. Similar to how it is done in the Basic setup, the TechDocs Reader requests `techdocs-backend` plugin for the docs site. `techdocs-backend` then requests your configured storage solution for the necessary files and returns them to TechDocs Reader. -We will provide instructions, scripts and/or templates (e.g. GitHub Actions) to -generate docs in your CI/CD system. -[Track progress here.](https://github.com/backstage/backstage/issues/3400) You -will be able to use `techdocs-cli` to generate docs and publish the generated -docs site files to your cloud storage system. - Note about caching: We have noticed internally that some storage providers can be quite slow, which is why we are recommending a cache that sits between the TechDocs Reader and the Storage. @@ -98,7 +90,7 @@ permissions to publish the generated docs site files. **Q: Why do you have separate "basic" and "recommended" deployment approaches?** A: The basic or out-of-the-box setup is what you get when you create a new app -or do a git clone of the Backstage repository. We want the first experience to +or do a `git clone` of the Backstage repository. We want the first experience to _just work magically_ so that you can have your first experience with TechDocs which is smooth. However, if you decide to deploy Backstage/TechDocs for production use, the basic setup would work but there are going to be downsides @@ -110,19 +102,21 @@ and we welcome such "Alternative" ideas from the community. **Q: Why don't you recommend techdocs-backend local filesystem to serve static files?** -A: It would make scaling a Backstage instance harder. Think about the case where -we have distributed Backstage deployments. Using a separate file storage system -for TechDocs makes it easier to do some operations like delete a docs site and -wipe its contents. +A: It makes scaling a Backstage instance harder. Think about the case where we +have distributed Backstage deployments (e.g. multiple Kubernetes pods of your +Backstage app). Using a separate/central file storage system for TechDocs is +necessary to make sure sites are persistent when the servers/pods are restarted +and avoid duplicating sites per instance. By having an external storage, we are +making it easier to do some operations like delete a docs site or wipe its +contents. **Q: Why aren't docs built on the fly i.e. when users visits a page, generate docs site in real-time?** -A: Generating the content from Markdown on the fly is not optimal (although that -is how the basic out-of-the-box setup is implemented). Storage solutions act as -a cache for the generated static content. TechDocs is also currently built on -MkDocs which does not allow us to generate docs per-page, so we would have to -build all docs for a entity on every request. +A: Generating the content from Markdown on the fly is not optimal. Storage +solutions act as a cache for the generated static content. TechDocs is also +currently built on MkDocs which does not allow us to generate docs per-page, so +we would have to build all docs for a entity on every request. **Q. Can you use the techdocs plugin without the techdocs-backend plugin?** @@ -147,21 +141,5 @@ We currently depend on MkDocs to parse doc sites written in Markdown. And we store the generated static assets and re-use it later to render in Backstage. A better (futuristic) approach will be to directly parse whatever type of source files you have in your docs repository and directly render in Backstage in -real-time. - -# Features status - -Status of all the features mentioned above. - -**In place ✅** - -- Basic setup with techdocs-backend file server as storage. -- Basic setup with cloud storage solution. -- `techdocs-cli` is able to generate docs in CI/CD environment. -- `techdocs-cli` is able to publish docs site to any storage. - -**Work in progress 🚧** - -**Not implemented yet ❌** - -- `techdocs-backend` integration with Backstage access control management. +real-time. You can read more in this +[RFC - Simplifying TechDocs Frontend Architecture](https://github.com/backstage/backstage/issues/3998). From 7fc89bae29cb71ea937f0d76261231e23a792169 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 29 Jan 2021 16:14:24 +0100 Subject: [PATCH 76/99] Display owner and system as entity page links in the tables of the `api-docs` plugin --- .changeset/empty-hairs-fetch.md | 13 +++ .../ApiExplorerTable/ApiExplorerTable.tsx | 82 ++++++++++++++++--- .../src/components/ApisCards/ApisTable.tsx | 81 +++++++++++++++--- .../ApisCards/ConsumedApisCard.test.tsx | 27 +++++- .../ApisCards/ProvidedApisCard.test.tsx | 27 +++++- .../ComponentsCards/ComponentsTable.tsx | 79 ++++++++++++++++-- .../ConsumingComponentsCard.test.tsx | 27 +++++- .../ProvidingComponentsCard.test.tsx | 27 +++++- .../components/EntityLink/EntityLink.test.tsx | 41 ---------- .../src/components/EntityLink/EntityLink.tsx | 43 ---------- plugins/catalog-react/src/index.ts | 1 + .../src/utils}/getEntityRelations.test.ts | 0 .../src/utils}/getEntityRelations.ts | 0 .../src/utils}/index.ts | 4 +- .../src/utils}/isOwnerOf.test.ts | 0 .../src/utils}/isOwnerOf.ts | 0 .../src/components/AboutCard/AboutContent.tsx | 6 +- .../components/CatalogPage/CatalogPage.tsx | 3 +- .../components/CatalogTable/CatalogTable.tsx | 2 +- .../Cards/OwnershipCard/OwnershipCard.tsx | 13 ++- .../org/src/components/getEntityRelations.ts | 42 ---------- plugins/org/src/components/isOwnerOf.ts | 55 ------------- 22 files changed, 339 insertions(+), 234 deletions(-) create mode 100644 .changeset/empty-hairs-fetch.md delete mode 100644 plugins/api-docs/src/components/EntityLink/EntityLink.test.tsx delete mode 100644 plugins/api-docs/src/components/EntityLink/EntityLink.tsx rename plugins/{catalog/src/components => catalog-react/src/utils}/getEntityRelations.test.ts (100%) rename plugins/{catalog/src/components => catalog-react/src/utils}/getEntityRelations.ts (100%) rename plugins/{api-docs/src/components/EntityLink => catalog-react/src/utils}/index.ts (85%) rename plugins/{catalog/src/components => catalog-react/src/utils}/isOwnerOf.test.ts (100%) rename plugins/{catalog/src/components => catalog-react/src/utils}/isOwnerOf.ts (100%) delete mode 100644 plugins/org/src/components/getEntityRelations.ts delete mode 100644 plugins/org/src/components/isOwnerOf.ts diff --git a/.changeset/empty-hairs-fetch.md b/.changeset/empty-hairs-fetch.md new file mode 100644 index 0000000000..aedeb8a853 --- /dev/null +++ b/.changeset/empty-hairs-fetch.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-api-docs': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-org': patch +--- + +Display owner and system as entity page links in the tables of the `api-docs` +plugin. + +Move `isOwnerOf` and `getEntityRelations` from `@backstage/plugin-catalog` to +`@backstage/plugin-catalog-react` and export it from there to use it by other +plugins. diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx index 149f7470c3..83af0ef216 100644 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx +++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx @@ -14,7 +14,13 @@ * limitations under the License. */ -import { ApiEntityV1alpha1, Entity } from '@backstage/catalog-model'; +import { + ApiEntityV1alpha1, + Entity, + EntityName, + RELATION_OWNED_BY, + RELATION_PART_OF, +} from '@backstage/catalog-model'; import { Table, TableColumn, @@ -22,24 +28,54 @@ import { TableState, useQueryParamState, } from '@backstage/core'; +import { + EntityRefLink, + EntityRefLinks, + formatEntityRefTitle, + getEntityRelations, +} from '@backstage/plugin-catalog-react'; import { Chip } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; import React from 'react'; import { ApiTypeTitle } from '../ApiDefinitionCard'; -import { EntityLink } from '../EntityLink'; -const columns: TableColumn[] = [ +type EntityRow = ApiEntityV1alpha1 & { + row: { + partOfSystemRelationTitle?: string; + partOfSystemRelations: EntityName[]; + ownedByRelationsTitle?: string; + ownedByRelations: EntityName[]; + }; +}; + +const columns: TableColumn[] = [ { title: 'Name', field: 'metadata.name', highlight: true, - render: (entity: any) => ( - {entity.metadata.name} + render: entity => ( + {entity.metadata.name} + ), + }, + { + title: 'System', + field: 'row.partOfSystemRelationTitle', + render: entity => ( + ), }, { title: 'Owner', - field: 'spec.owner', + field: 'row.ownedByRelationsTitle', + render: entity => ( + + ), }, { title: 'Lifecycle', @@ -48,9 +84,7 @@ const columns: TableColumn[] = [ { title: 'Type', field: 'spec.type', - render: (entity: Entity) => ( - - ), + render: entity => , }, { title: 'Description', @@ -62,7 +96,7 @@ const columns: TableColumn[] = [ cellStyle: { padding: '0px 16px 0px 20px', }, - render: (entity: Entity) => ( + render: entity => ( <> {entity.metadata.tags && entity.metadata.tags.map(t => ( @@ -123,8 +157,32 @@ export const ApiExplorerTable = ({ ); } + const rows = entities.map(e => { + const partOfSystemRelations = getEntityRelations(e, RELATION_PART_OF, { + kind: 'system', + }); + const ownedByRelations = getEntityRelations(e, RELATION_OWNED_BY); + + return { + ...(e as ApiEntityV1alpha1), + row: { + ownedByRelationsTitle: ownedByRelations + .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) + .join(', '), + ownedByRelations, + partOfSystemRelationTitle: + partOfSystemRelations.length > 0 + ? formatEntityRefTitle(partOfSystemRelations[0], { + defaultKind: 'system', + }) + : undefined, + partOfSystemRelations, + }, + }; + }); + return ( - isLoading={loading} columns={columns} options={{ @@ -134,7 +192,7 @@ export const ApiExplorerTable = ({ padding: 'dense', showEmptyDataSourceMessage: !loading, }} - data={entities} + data={rows} filters={filters} initialState={queryParamState} onStateChange={setQueryParamState} diff --git a/plugins/api-docs/src/components/ApisCards/ApisTable.tsx b/plugins/api-docs/src/components/ApisCards/ApisTable.tsx index 7db62433eb..56ba697234 100644 --- a/plugins/api-docs/src/components/ApisCards/ApisTable.tsx +++ b/plugins/api-docs/src/components/ApisCards/ApisTable.tsx @@ -14,24 +14,59 @@ * limitations under the License. */ -import { ApiEntity } from '@backstage/catalog-model'; +import { + ApiEntity, + EntityName, + RELATION_OWNED_BY, + RELATION_PART_OF, +} from '@backstage/catalog-model'; import { Table, TableColumn } from '@backstage/core'; +import { + EntityRefLink, + EntityRefLinks, + formatEntityRefTitle, + getEntityRelations, +} from '@backstage/plugin-catalog-react'; import React from 'react'; import { ApiTypeTitle } from '../ApiDefinitionCard'; -import { EntityLink } from '../EntityLink'; -const columns: TableColumn[] = [ +type EntityRow = ApiEntity & { + row: { + partOfSystemRelationTitle?: string; + partOfSystemRelations: EntityName[]; + ownedByRelationsTitle?: string; + ownedByRelations: EntityName[]; + }; +}; + +const columns: TableColumn[] = [ { title: 'Name', field: 'metadata.name', highlight: true, - render: (entity: any) => ( - {entity.metadata.name} + render: entity => ( + {entity.metadata.name} + ), + }, + { + title: 'System', + field: 'row.partOfSystemRelationTitle', + render: entity => ( + ), }, { title: 'Owner', - field: 'spec.owner', + field: 'row.ownedByRelationsTitle', + render: entity => ( + + ), }, { title: 'Lifecycle', @@ -40,7 +75,7 @@ const columns: TableColumn[] = [ { title: 'Type', field: 'spec.type', - render: (entity: ApiEntity) => , + render: entity => , }, { title: 'Description', @@ -65,8 +100,35 @@ export const ApisTable = ({ entities, title, variant = 'gridItem' }: Props) => { tableStyle.height = 'calc(100% - 10px)'; } + const rows = entities + // TODO: For now we skip all APIs that we can't find without a warning! + .filter(e => e !== undefined) + .map(e => { + const partOfSystemRelations = getEntityRelations(e, RELATION_PART_OF, { + kind: 'system', + }); + const ownedByRelations = getEntityRelations(e, RELATION_OWNED_BY); + + return { + ...(e as ApiEntity), + row: { + ownedByRelationsTitle: ownedByRelations + .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) + .join(', '), + ownedByRelations, + partOfSystemRelationTitle: + partOfSystemRelations.length > 0 + ? formatEntityRefTitle(partOfSystemRelations[0], { + defaultKind: 'system', + }) + : undefined, + partOfSystemRelations, + }, + }; + }); + return ( - + columns={columns} title={title} style={tableStyle} @@ -77,8 +139,7 @@ export const ApisTable = ({ entities, title, variant = 'gridItem' }: Props) => { actionsColumnIndex: -1, padding: 'dense', }} - // TODO: For now we skip all APIs that we can't find without a warning! - data={entities.filter(e => e !== undefined) as ApiEntity[]} + data={rows} /> ); }; diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx index d406e160fc..54cf08a429 100644 --- a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx @@ -14,7 +14,12 @@ * limitations under the License. */ -import { Entity, RELATION_CONSUMES_API } from '@backstage/catalog-model'; +import { + Entity, + RELATION_CONSUMES_API, + RELATION_OWNED_BY, + RELATION_PART_OF, +} from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry } from '@backstage/core'; import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; @@ -99,10 +104,27 @@ describe('', () => { }, spec: { type: 'openapi', - owner: 'Test', lifecycle: 'production', definition: '...', }, + relations: [ + { + type: RELATION_PART_OF, + target: { + kind: 'System', + name: 'MySystem', + namespace: 'default', + }, + }, + { + type: RELATION_OWNED_BY, + target: { + kind: 'Group', + name: 'Test', + namespace: 'default', + }, + }, + ], }); apiDocsConfig.getApiDefinitionWidget.mockReturnValue({ type: 'openapi', @@ -121,6 +143,7 @@ describe('', () => { expect(getByText(/target-name/i)).toBeInTheDocument(); expect(getByText(/OpenAPI/)).toBeInTheDocument(); expect(getByText(/Test/i)).toBeInTheDocument(); + expect(getByText(/MySystem/i)).toBeInTheDocument(); expect(getByText(/production/i)).toBeInTheDocument(); }); }); diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx index 7b6ce6e18a..4300aeff6d 100644 --- a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx @@ -14,7 +14,12 @@ * limitations under the License. */ -import { Entity, RELATION_PROVIDES_API } from '@backstage/catalog-model'; +import { + Entity, + RELATION_OWNED_BY, + RELATION_PART_OF, + RELATION_PROVIDES_API, +} from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry } from '@backstage/core'; import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; @@ -99,10 +104,27 @@ describe('', () => { }, spec: { type: 'openapi', - owner: 'Test', lifecycle: 'production', definition: '...', }, + relations: [ + { + type: RELATION_PART_OF, + target: { + kind: 'System', + name: 'MySystem', + namespace: 'default', + }, + }, + { + type: RELATION_OWNED_BY, + target: { + kind: 'Group', + name: 'Test', + namespace: 'default', + }, + }, + ], }); apiDocsConfig.getApiDefinitionWidget.mockReturnValue({ type: 'openapi', @@ -120,6 +142,7 @@ describe('', () => { expect(getByText(/Provided APIs/i)).toBeInTheDocument(); expect(getByText(/target-name/i)).toBeInTheDocument(); expect(getByText(/OpenAPI/)).toBeInTheDocument(); + expect(getByText(/MySystem/)).toBeInTheDocument(); expect(getByText(/Test/i)).toBeInTheDocument(); expect(getByText(/production/i)).toBeInTheDocument(); }); diff --git a/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx b/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx index 1b62a56d10..2a6115f512 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx @@ -14,23 +14,58 @@ * limitations under the License. */ -import { ComponentEntity } from '@backstage/catalog-model'; +import { + ComponentEntity, + EntityName, + RELATION_OWNED_BY, + RELATION_PART_OF, +} from '@backstage/catalog-model'; import { Table, TableColumn } from '@backstage/core'; +import { + EntityRefLink, + EntityRefLinks, + formatEntityRefTitle, + getEntityRelations, +} from '@backstage/plugin-catalog-react'; import React from 'react'; -import { EntityLink } from '../EntityLink'; -const columns: TableColumn[] = [ +type EntityRow = ComponentEntity & { + row: { + partOfSystemRelationTitle?: string; + partOfSystemRelations: EntityName[]; + ownedByRelationsTitle?: string; + ownedByRelations: EntityName[]; + }; +}; + +const columns: TableColumn[] = [ { title: 'Name', field: 'metadata.name', highlight: true, - render: (entity: any) => ( - {entity.metadata.name} + render: entity => ( + {entity.metadata.name} + ), + }, + { + title: 'System', + field: 'row.partOfSystemRelationTitle', + render: entity => ( + ), }, { title: 'Owner', - field: 'spec.owner', + field: 'row.ownedByRelationsTitle', + render: entity => ( + + ), }, { title: 'Lifecycle', @@ -68,8 +103,35 @@ export const ComponentsTable = ({ tableStyle.height = 'calc(100% - 10px)'; } + const rows = entities + // TODO: For now we skip all Components that we can't find without a warning! + .filter(e => e !== undefined) + .map(e => { + const partOfSystemRelations = getEntityRelations(e, RELATION_PART_OF, { + kind: 'system', + }); + const ownedByRelations = getEntityRelations(e, RELATION_OWNED_BY); + + return { + ...(e as ComponentEntity), + row: { + ownedByRelationsTitle: ownedByRelations + .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) + .join(', '), + ownedByRelations, + partOfSystemRelationTitle: + partOfSystemRelations.length > 0 + ? formatEntityRefTitle(partOfSystemRelations[0], { + defaultKind: 'system', + }) + : undefined, + partOfSystemRelations, + }, + }; + }); + return ( - + columns={columns} title={title} style={tableStyle} @@ -80,8 +142,7 @@ export const ComponentsTable = ({ actionsColumnIndex: -1, padding: 'dense', }} - // TODO: For now we skip all APIs that we can't find without a warning! - data={entities.filter(e => e !== undefined) as ComponentEntity[]} + data={rows} /> ); }; diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx index 99a8c0dc28..de66b2af06 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx @@ -14,7 +14,12 @@ * limitations under the License. */ -import { Entity, RELATION_API_CONSUMED_BY } from '@backstage/catalog-model'; +import { + Entity, + RELATION_API_CONSUMED_BY, + RELATION_OWNED_BY, + RELATION_PART_OF, +} from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry } from '@backstage/core'; import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; @@ -104,9 +109,26 @@ describe('', () => { }, spec: { type: 'service', - owner: 'Test', lifecycle: 'production', }, + relations: [ + { + type: RELATION_PART_OF, + target: { + kind: 'System', + name: 'MySystem', + namespace: 'default', + }, + }, + { + type: RELATION_OWNED_BY, + target: { + kind: 'Group', + name: 'Test', + namespace: 'default', + }, + }, + ], }); const { getByText } = await renderInTestApp( @@ -119,6 +141,7 @@ describe('', () => { expect(getByText(/Consumers/i)).toBeInTheDocument(); expect(getByText(/target-name/i)).toBeInTheDocument(); expect(getByText(/Test/i)).toBeInTheDocument(); + expect(getByText(/MySystem/i)).toBeInTheDocument(); expect(getByText(/production/i)).toBeInTheDocument(); }); }); diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx index a3341ad8d0..b7b1616e00 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx @@ -14,7 +14,12 @@ * limitations under the License. */ -import { Entity, RELATION_API_PROVIDED_BY } from '@backstage/catalog-model'; +import { + Entity, + RELATION_API_PROVIDED_BY, + RELATION_OWNED_BY, + RELATION_PART_OF, +} from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry } from '@backstage/core'; import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; @@ -104,9 +109,26 @@ describe('', () => { }, spec: { type: 'service', - owner: 'Test', lifecycle: 'production', }, + relations: [ + { + type: RELATION_PART_OF, + target: { + kind: 'System', + name: 'MySystem', + namespace: 'default', + }, + }, + { + type: RELATION_OWNED_BY, + target: { + kind: 'Group', + name: 'Test', + namespace: 'default', + }, + }, + ], }); const { getByText } = await renderInTestApp( @@ -119,6 +141,7 @@ describe('', () => { expect(getByText(/Providers/i)).toBeInTheDocument(); expect(getByText(/target-name/i)).toBeInTheDocument(); expect(getByText(/Test/i)).toBeInTheDocument(); + expect(getByText(/MySystem/i)).toBeInTheDocument(); expect(getByText(/production/i)).toBeInTheDocument(); }); }); diff --git a/plugins/api-docs/src/components/EntityLink/EntityLink.test.tsx b/plugins/api-docs/src/components/EntityLink/EntityLink.test.tsx deleted file mode 100644 index 9221b96604..0000000000 --- a/plugins/api-docs/src/components/EntityLink/EntityLink.test.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Entity } from '@backstage/catalog-model'; -import { renderInTestApp } from '@backstage/test-utils'; -import React from 'react'; -import { EntityLink } from './EntityLink'; - -describe('', () => { - it('provides link to entity page', async () => { - const entity: Entity = { - apiVersion: 'v1', - kind: 'Component', - metadata: { - name: 'my-name', - namespace: 'my-namespace', - }, - }; - const { getByText } = await renderInTestApp( - inner, - ); - expect(getByText(/inner/i)).toBeInTheDocument(); - expect(getByText(/inner/i)).toHaveAttribute( - 'href', - '/catalog/my-namespace/component/my-name', - ); - }); -}); diff --git a/plugins/api-docs/src/components/EntityLink/EntityLink.tsx b/plugins/api-docs/src/components/EntityLink/EntityLink.tsx deleted file mode 100644 index dba6bb0062..0000000000 --- a/plugins/api-docs/src/components/EntityLink/EntityLink.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Entity } from '@backstage/catalog-model'; -import { - entityRoute, - entityRouteParams, -} from '@backstage/plugin-catalog-react'; -import { Link } from '@material-ui/core'; -import React, { PropsWithChildren } from 'react'; -import { generatePath, Link as RouterLink } from 'react-router-dom'; - -type Props = { - entity: Entity; -}; - -// TODO: Could be useful for others too, as part of the catalog plugin -export const EntityLink = ({ entity, children }: PropsWithChildren) => { - return ( - - {children} - - ); -}; diff --git a/plugins/catalog-react/src/index.ts b/plugins/catalog-react/src/index.ts index 9b6e13bb26..af3eca4e0a 100644 --- a/plugins/catalog-react/src/index.ts +++ b/plugins/catalog-react/src/index.ts @@ -24,3 +24,4 @@ export { entityRouteRef, rootRoute, } from './routes'; +export * from './utils'; diff --git a/plugins/catalog/src/components/getEntityRelations.test.ts b/plugins/catalog-react/src/utils/getEntityRelations.test.ts similarity index 100% rename from plugins/catalog/src/components/getEntityRelations.test.ts rename to plugins/catalog-react/src/utils/getEntityRelations.test.ts diff --git a/plugins/catalog/src/components/getEntityRelations.ts b/plugins/catalog-react/src/utils/getEntityRelations.ts similarity index 100% rename from plugins/catalog/src/components/getEntityRelations.ts rename to plugins/catalog-react/src/utils/getEntityRelations.ts diff --git a/plugins/api-docs/src/components/EntityLink/index.ts b/plugins/catalog-react/src/utils/index.ts similarity index 85% rename from plugins/api-docs/src/components/EntityLink/index.ts rename to plugins/catalog-react/src/utils/index.ts index 44875e4634..2efb35703e 100644 --- a/plugins/api-docs/src/components/EntityLink/index.ts +++ b/plugins/catalog-react/src/utils/index.ts @@ -13,5 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -export { EntityLink } from './EntityLink'; +export { getEntityRelations } from './getEntityRelations'; +export { isOwnerOf } from './isOwnerOf'; diff --git a/plugins/catalog/src/components/isOwnerOf.test.ts b/plugins/catalog-react/src/utils/isOwnerOf.test.ts similarity index 100% rename from plugins/catalog/src/components/isOwnerOf.test.ts rename to plugins/catalog-react/src/utils/isOwnerOf.test.ts diff --git a/plugins/catalog/src/components/isOwnerOf.ts b/plugins/catalog-react/src/utils/isOwnerOf.ts similarity index 100% rename from plugins/catalog/src/components/isOwnerOf.ts rename to plugins/catalog-react/src/utils/isOwnerOf.ts diff --git a/plugins/catalog/src/components/AboutCard/AboutContent.tsx b/plugins/catalog/src/components/AboutCard/AboutContent.tsx index bb55e00e93..75b7827111 100644 --- a/plugins/catalog/src/components/AboutCard/AboutContent.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutContent.tsx @@ -19,10 +19,12 @@ import { RELATION_OWNED_BY, RELATION_PART_OF, } from '@backstage/catalog-model'; -import { EntityRefLinks } from '@backstage/plugin-catalog-react'; +import { + EntityRefLinks, + getEntityRelations, +} from '@backstage/plugin-catalog-react'; import { Chip, Grid, makeStyles, Typography } from '@material-ui/core'; import React from 'react'; -import { getEntityRelations } from '../getEntityRelations'; import { AboutField } from './AboutField'; const useStyles = makeStyles({ diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 0384c75c96..1298d5e190 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -22,7 +22,7 @@ import { SupportButton, useApi, } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { catalogApiRef, isOwnerOf } from '@backstage/plugin-catalog-react'; import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder'; import { Button, makeStyles } from '@material-ui/core'; import SettingsIcon from '@material-ui/icons/Settings'; @@ -33,7 +33,6 @@ import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter'; import { useStarredEntities } from '../../hooks/useStarredEntities'; import { ButtonGroup, CatalogFilter } from '../CatalogFilter/CatalogFilter'; import { CatalogTable } from '../CatalogTable/CatalogTable'; -import { isOwnerOf } from '../isOwnerOf'; import { ResultsFilter } from '../ResultsFilter/ResultsFilter'; import { useOwnUser } from '../useOwnUser'; import CatalogLayout from './CatalogLayout'; diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index ed0becf270..049b2a2052 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -24,6 +24,7 @@ import { EntityRefLink, EntityRefLinks, formatEntityRefTitle, + getEntityRelations, } from '@backstage/plugin-catalog-react'; import { Chip } from '@material-ui/core'; import Edit from '@material-ui/icons/Edit'; @@ -37,7 +38,6 @@ import { favouriteEntityIcon, favouriteEntityTooltip, } from '../FavouriteEntity/FavouriteEntity'; -import { getEntityRelations } from '../getEntityRelations'; type EntityRow = Entity & { row: { diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx index 4f59c4be69..60f06bd53a 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx @@ -14,12 +14,10 @@ * limitations under the License. */ -import React from 'react'; -import { InfoCard, useApi, Progress } from '@backstage/core'; import { Entity } from '@backstage/catalog-model'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { useAsync } from 'react-use'; -import Alert from '@material-ui/lab/Alert'; +import { InfoCard, Progress, useApi } from '@backstage/core'; +import { catalogApiRef, isOwnerOf } from '@backstage/plugin-catalog-react'; +import { pageTheme } from '@backstage/theme'; import { Box, createStyles, @@ -28,8 +26,9 @@ import { Theme, Typography, } from '@material-ui/core'; -import { pageTheme } from '@backstage/theme'; -import { isOwnerOf } from '../../isOwnerOf'; +import Alert from '@material-ui/lab/Alert'; +import React from 'react'; +import { useAsync } from 'react-use'; type EntitiesKinds = 'Component' | 'API'; type EntitiesTypes = diff --git a/plugins/org/src/components/getEntityRelations.ts b/plugins/org/src/components/getEntityRelations.ts deleted file mode 100644 index 9230aaebee..0000000000 --- a/plugins/org/src/components/getEntityRelations.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Entity, EntityName } from '@backstage/catalog-model'; - -// TODO: this file is copied from /packages/app/catalog/src/components/getEntityRelations.ts and -// should be replaced once common relation-functions are introduced. - -/** - * Get the related entity references. - */ -export function getEntityRelations( - entity: Entity | undefined, - relationType: string, - filter?: { kind: string }, -): EntityName[] { - let entityNames = - entity?.relations - ?.filter(r => r.type === relationType) - ?.map(r => r.target) || []; - - if (filter?.kind) { - entityNames = entityNames?.filter( - e => e.kind.toLowerCase() === filter.kind.toLowerCase(), - ); - } - - return entityNames; -} diff --git a/plugins/org/src/components/isOwnerOf.ts b/plugins/org/src/components/isOwnerOf.ts deleted file mode 100644 index dd7c7d6805..0000000000 --- a/plugins/org/src/components/isOwnerOf.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - Entity, - EntityName, - getEntityName, - RELATION_MEMBER_OF, - RELATION_OWNED_BY, -} from '@backstage/catalog-model'; -import { getEntityRelations } from './getEntityRelations'; - -// TODO: this file is copied from /packages/app/catalog/src/components/isOwnerOf.ts and -// should be replaced once common relation-functions are introduced. - -/** - * Check if one entity is owned by another. Returns true, if the entity is owned by the - * owner directly, or if the entity is owned by a group that the owner is a member of. - */ -export function isOwnerOf(owner: Entity, owned: Entity) { - const possibleOwners: EntityName[] = [ - ...getEntityRelations(owner, RELATION_MEMBER_OF, { kind: 'group' }), - ...(owner ? [getEntityName(owner)] : []), - ]; - - const owners = getEntityRelations(owned, RELATION_OWNED_BY); - - for (const owner of owners) { - if ( - possibleOwners.find( - o => - owner.kind.toLowerCase() === o.kind.toLowerCase() && - owner.namespace.toLowerCase() === o.namespace.toLowerCase() && - owner.name.toLowerCase() === o.name.toLowerCase(), - ) !== undefined - ) { - return true; - } - } - - return false; -} From 5ac9df899520bb4d35364636fc923f0825c9cf48 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 1 Feb 2021 12:19:04 +0100 Subject: [PATCH 77/99] Ports the Fossa Plugin to the new plugin model Towards #3424 --- .changeset/pretty-melons-prove.md | 8 ++ plugins/fossa/README.md | 8 +- plugins/fossa/dev/index.tsx | 132 +++++++++++++++--------------- plugins/fossa/package.json | 1 + plugins/fossa/src/extensions.tsx | 35 ++++++++ plugins/fossa/src/index.ts | 4 +- plugins/fossa/src/plugin.test.ts | 4 +- plugins/fossa/src/plugin.ts | 2 +- 8 files changed, 119 insertions(+), 75 deletions(-) create mode 100644 .changeset/pretty-melons-prove.md create mode 100644 plugins/fossa/src/extensions.tsx diff --git a/.changeset/pretty-melons-prove.md b/.changeset/pretty-melons-prove.md new file mode 100644 index 0000000000..7048757e5a --- /dev/null +++ b/.changeset/pretty-melons-prove.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-fossa': minor +--- + +Port FOSSA plugin to new extension model. + +If you are using the FOSSA plugin adjust the plugin import from `plugin` to +`fossaPlugin` and replace `` with ``. diff --git a/plugins/fossa/README.md b/plugins/fossa/README.md index 489aef90ab..6e53ced0cf 100644 --- a/plugins/fossa/README.md +++ b/plugins/fossa/README.md @@ -19,21 +19,21 @@ yarn add @backstage/plugin-fossa ```js // packages/app/src/plugins.ts -export { plugin as Fossa } from '@backstage/plugin-fossa'; +export { fossaPlugin } from '@backstage/plugin-fossa'; ``` -3. Add the `FossaCard` to the EntityPage: +3. Add the `EntityFossaCard` to the EntityPage: ```jsx // packages/app/src/components/catalog/EntityPage.tsx -import { FossaCard } from '@backstage/plugin-fossa'; +import { EntityFossaCard } from '@backstage/plugin-fossa'; const OverviewContent = ({ entity }: { entity: Entity }) => ( // ... - + // ... diff --git a/plugins/fossa/dev/index.tsx b/plugins/fossa/dev/index.tsx index 7846324b25..06c71c48b7 100644 --- a/plugins/fossa/dev/index.tsx +++ b/plugins/fossa/dev/index.tsx @@ -14,21 +14,28 @@ * limitations under the License. */ -import { createDevApp } from '@backstage/dev-utils'; -import { - Content, - createPlugin, - createRouteRef, - Header, - Page, -} from '@backstage/core'; -import React from 'react'; -import { Grid } from '@material-ui/core'; -import { FossaApi, fossaApiRef } from '../src/api'; -import { FossaCard } from '../src'; import { Entity } from '@backstage/catalog-model'; +import { Content, Header, Page } from '@backstage/core'; +import { createDevApp } from '@backstage/dev-utils'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { Grid } from '@material-ui/core'; +import React from 'react'; +import { EntityFossaCard } from '../src'; +import { FossaApi, fossaApiRef } from '../src/api'; import { FOSSA_PROJECT_NAME_ANNOTATION } from '../src/components/useProjectName'; +const entity = (name?: string) => + ({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + annotations: { + [FOSSA_PROJECT_NAME_ANNOTATION]: name, + }, + name: name, + }, + } as Entity); + createDevApp() .registerApi({ api: fossaApiRef, @@ -76,58 +83,51 @@ createDevApp() }, } as FossaApi), }) - .registerPlugin( - createPlugin({ - id: 'fossa-demo', - register({ router }) { - const entity = (name?: string) => - ({ - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - annotations: { - [FOSSA_PROJECT_NAME_ANNOTATION]: name, - }, - name: name, - }, - } as Entity); - - const ExamplePage = () => ( - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - ); - - router.addRoute( - createRouteRef({ path: '/', title: 'Fossa' }), - ExamplePage, - ); - }, - }), - ) + .addPage({ + title: 'Entity Content', + element: ( + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + }) .render(); diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index b69039fa00..879f12c6f6 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -33,6 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", + "@backstage/plugin-catalog-react": "^0.0.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/fossa/src/extensions.tsx b/plugins/fossa/src/extensions.tsx new file mode 100644 index 0000000000..e2bfeed134 --- /dev/null +++ b/plugins/fossa/src/extensions.tsx @@ -0,0 +1,35 @@ +/* + * 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 { createComponentExtension } from '@backstage/core'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import React from 'react'; +import { fossaPlugin } from './plugin'; + +export const EntityFossaCard = fossaPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/FossaCard').then(({ FossaCard }) => { + const EntityFossaCard = () => { + const { entity } = useEntity(); + return ; + }; + return EntityFossaCard; + }), + }, + }), +); diff --git a/plugins/fossa/src/index.ts b/plugins/fossa/src/index.ts index c35c534122..f1b645c29b 100644 --- a/plugins/fossa/src/index.ts +++ b/plugins/fossa/src/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { plugin } from './plugin'; -export * from './components'; +export { fossaPlugin } from './plugin'; +export { EntityFossaCard } from './extensions'; diff --git a/plugins/fossa/src/plugin.test.ts b/plugins/fossa/src/plugin.test.ts index 4f9b00a02b..9aed1387c5 100644 --- a/plugins/fossa/src/plugin.test.ts +++ b/plugins/fossa/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { fossaPlugin } from './plugin'; describe('fossa', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(fossaPlugin).toBeDefined(); }); }); diff --git a/plugins/fossa/src/plugin.ts b/plugins/fossa/src/plugin.ts index d1fe621018..cb1de3911f 100644 --- a/plugins/fossa/src/plugin.ts +++ b/plugins/fossa/src/plugin.ts @@ -22,7 +22,7 @@ import { } from '@backstage/core'; import { fossaApiRef, FossaClient } from './api'; -export const plugin = createPlugin({ +export const fossaPlugin = createPlugin({ id: 'fossa', apis: [ createApiFactory({ From ac14e108d7486c2c29a802e9c8b4822a4a07209f Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 1 Feb 2021 12:32:37 +0100 Subject: [PATCH 78/99] Simplify code --- .../src/components/FossaCard/FossaCard.tsx | 21 +++++++------------ plugins/fossa/src/extensions.tsx | 11 +--------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/plugins/fossa/src/components/FossaCard/FossaCard.tsx b/plugins/fossa/src/components/FossaCard/FossaCard.tsx index d1ac06d365..4cc27f5d03 100644 --- a/plugins/fossa/src/components/FossaCard/FossaCard.tsx +++ b/plugins/fossa/src/components/FossaCard/FossaCard.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import React from 'react'; import { EmptyState, InfoCard, @@ -22,15 +21,16 @@ import { Progress, useApi, } from '@backstage/core'; -import { useAsync } from 'react-use'; -import { Entity } from '@backstage/catalog-model'; -import { fossaApiRef } from '../../api'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { Grid, Tooltip } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; +import React from 'react'; +import { useAsync } from 'react-use'; +import { fossaApiRef } from '../../api'; import { FOSSA_PROJECT_NAME_ANNOTATION, useProjectName, } from '../useProjectName'; -import { Grid, Tooltip } from '@material-ui/core'; const useStyles = makeStyles(theme => ({ numberError: { @@ -65,13 +65,8 @@ const useStyles = makeStyles(theme => ({ }, })); -export const FossaCard = ({ - entity, - variant = 'gridItem', -}: { - entity: Entity; - variant?: string; -}) => { +export const FossaCard = () => { + const { entity } = useEntity(); const fossaApi = useApi(fossaApiRef); const projectTitle = useProjectName(entity); @@ -96,7 +91,7 @@ export const FossaCard = ({ - import('./components/FossaCard').then(({ FossaCard }) => { - const EntityFossaCard = () => { - const { entity } = useEntity(); - return ; - }; - return EntityFossaCard; - }), + lazy: () => import('./components/FossaCard').then(m => m.FossaCard), }, }), ); From 3bdd6eac0780d3d7298b50cb129e53a340b94e72 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 1 Feb 2021 13:36:46 +0100 Subject: [PATCH 79/99] techdocs: Fix broken tests on Windows https://github.com/backstage/backstage/runs/1805649470\?check_suite_focus\=true --- .../src/stages/publish/azureBlobStorage.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts index 9d2ee1ba9b..80f3cb0cf7 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts @@ -115,10 +115,8 @@ describe('AzureBlobStoragePublish', () => { directory: wrongPathToGeneratedDirectory, }) .catch(error => - expect(error).toEqual( - new Error( - `Unable to upload file(s) to Azure Blob Storage. Error Failed to read template directory: ENOENT, no such file or directory '${wrongPathToGeneratedDirectory}'`, - ), + expect(error.message).toContain( + 'Unable to upload file(s) to Azure Blob Storage. Error Failed to read template directory: ENOENT, no such file or directory', ), ); mockFs.restore(); From 5d42c0421825f0f1e6d97cd4ff6bc989c8969cdb Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Mon, 1 Feb 2021 13:41:14 +0100 Subject: [PATCH 80/99] Update docs/features/software-templates/index.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw --- docs/features/software-templates/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-templates/index.md b/docs/features/software-templates/index.md index 026c3f4bfb..701b38276a 100644 --- a/docs/features/software-templates/index.md +++ b/docs/features/software-templates/index.md @@ -41,7 +41,7 @@ internally. After filling in these variables, you'll get some more fields to fill out which are required for Backstage usage: the owner (which is a `user` in the backstage system), and the `storePath` which is a destination URL to create for the -provider for instance http://github.com/backstage/my-new-repository, or +provider, for instance `https://github.com/backstage/my-new-repository`, or https://gitlab.com/myorg/myrepo. ![Enter Backstage vars](../../assets/software-templates/template-picked-2.png) From c0a5e2710aff50cc9c123e94d26351b280f2119c Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Mon, 1 Feb 2021 13:41:20 +0100 Subject: [PATCH 81/99] Update docs/features/software-templates/index.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw --- docs/features/software-templates/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-templates/index.md b/docs/features/software-templates/index.md index 701b38276a..12ce6e3ed3 100644 --- a/docs/features/software-templates/index.md +++ b/docs/features/software-templates/index.md @@ -42,7 +42,7 @@ After filling in these variables, you'll get some more fields to fill out which are required for Backstage usage: the owner (which is a `user` in the backstage system), and the `storePath` which is a destination URL to create for the provider, for instance `https://github.com/backstage/my-new-repository`, or -https://gitlab.com/myorg/myrepo. +`https://gitlab.com/myorg/myrepo`. ![Enter Backstage vars](../../assets/software-templates/template-picked-2.png) From e3d9da115c72f6fe78e7da5188f81dd0a6d166b9 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 1 Feb 2021 13:53:18 +0100 Subject: [PATCH 82/99] Correctly set the table value for sort/filtering --- .../ApiExplorerTable/ApiExplorerTable.tsx | 13 +++++++------ .../api-docs/src/components/ApisCards/ApisTable.tsx | 13 +++++++------ .../components/ComponentsCards/ComponentsTable.tsx | 13 +++++++------ .../src/components/CatalogTable/CatalogTable.tsx | 13 +++++++------ 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx index 83af0ef216..7aa09a2c02 100644 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx +++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx @@ -170,12 +170,13 @@ export const ApiExplorerTable = ({ .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) .join(', '), ownedByRelations, - partOfSystemRelationTitle: - partOfSystemRelations.length > 0 - ? formatEntityRefTitle(partOfSystemRelations[0], { - defaultKind: 'system', - }) - : undefined, + partOfSystemRelationTitle: partOfSystemRelations + .map(r => + formatEntityRefTitle(r, { + defaultKind: 'system', + }), + ) + .join(', '), partOfSystemRelations, }, }; diff --git a/plugins/api-docs/src/components/ApisCards/ApisTable.tsx b/plugins/api-docs/src/components/ApisCards/ApisTable.tsx index 56ba697234..0595044327 100644 --- a/plugins/api-docs/src/components/ApisCards/ApisTable.tsx +++ b/plugins/api-docs/src/components/ApisCards/ApisTable.tsx @@ -116,12 +116,13 @@ export const ApisTable = ({ entities, title, variant = 'gridItem' }: Props) => { .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) .join(', '), ownedByRelations, - partOfSystemRelationTitle: - partOfSystemRelations.length > 0 - ? formatEntityRefTitle(partOfSystemRelations[0], { - defaultKind: 'system', - }) - : undefined, + partOfSystemRelationTitle: partOfSystemRelations + .map(r => + formatEntityRefTitle(r, { + defaultKind: 'system', + }), + ) + .join(', '), partOfSystemRelations, }, }; diff --git a/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx b/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx index 2a6115f512..1ac82991eb 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx @@ -119,12 +119,13 @@ export const ComponentsTable = ({ .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) .join(', '), ownedByRelations, - partOfSystemRelationTitle: - partOfSystemRelations.length > 0 - ? formatEntityRefTitle(partOfSystemRelations[0], { - defaultKind: 'system', - }) - : undefined, + partOfSystemRelationTitle: partOfSystemRelations + .map(r => + formatEntityRefTitle(r, { + defaultKind: 'system', + }), + ) + .join(', '), partOfSystemRelations, }, }; diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 049b2a2052..6f91b250df 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -180,12 +180,13 @@ export const CatalogTable = ({ .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) .join(', '), ownedByRelations, - partOfSystemRelationTitle: - partOfSystemRelations.length > 0 - ? formatEntityRefTitle(partOfSystemRelations[0], { - defaultKind: 'system', - }) - : undefined, + partOfSystemRelationTitle: partOfSystemRelations + .map(r => + formatEntityRefTitle(r, { + defaultKind: 'system', + }), + ) + .join(', '), partOfSystemRelations, }, }; From e646d8065a0fb79f8c0883839c0cb46dbee36f64 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 1 Feb 2021 14:29:24 +0100 Subject: [PATCH 83/99] Change EntityRow to have an entity and a resolved field instead of combining them --- .../ApiExplorerTable/ApiExplorerTable.tsx | 43 ++++++++------- .../src/components/ApisCards/ApisTable.tsx | 47 +++++++++------- .../ComponentsCards/ComponentsTable.tsx | 45 ++++++++------- .../components/CatalogTable/CatalogTable.tsx | 55 ++++++++++--------- 4 files changed, 101 insertions(+), 89 deletions(-) diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx index 7aa09a2c02..6baacf5961 100644 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx +++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx @@ -39,8 +39,9 @@ import { Alert } from '@material-ui/lab'; import React from 'react'; import { ApiTypeTitle } from '../ApiDefinitionCard'; -type EntityRow = ApiEntityV1alpha1 & { - row: { +type EntityRow = { + entity: ApiEntityV1alpha1; + resolved: { partOfSystemRelationTitle?: string; partOfSystemRelations: EntityName[]; ownedByRelationsTitle?: string; @@ -51,52 +52,52 @@ type EntityRow = ApiEntityV1alpha1 & { const columns: TableColumn[] = [ { title: 'Name', - field: 'metadata.name', + field: 'entity.metadata.name', highlight: true, - render: entity => ( + render: ({ entity }) => ( {entity.metadata.name} ), }, { title: 'System', - field: 'row.partOfSystemRelationTitle', - render: entity => ( + field: 'resolved.partOfSystemRelationTitle', + render: ({ resolved }) => ( ), }, { title: 'Owner', - field: 'row.ownedByRelationsTitle', - render: entity => ( + field: 'resolved.ownedByRelationsTitle', + render: ({ resolved }) => ( ), }, { title: 'Lifecycle', - field: 'spec.lifecycle', + field: 'entity.spec.lifecycle', }, { title: 'Type', - field: 'spec.type', - render: entity => , + field: 'entity.spec.type', + render: ({ entity }) => , }, { title: 'Description', - field: 'metadata.description', + field: 'entity.metadata.description', }, { title: 'Tags', - field: 'metadata.tags', + field: 'entity.metadata.tags', cellStyle: { padding: '0px 16px 0px 20px', }, - render: entity => ( + render: ({ entity }) => ( <> {entity.metadata.tags && entity.metadata.tags.map(t => ( @@ -157,15 +158,15 @@ export const ApiExplorerTable = ({ ); } - const rows = entities.map(e => { - const partOfSystemRelations = getEntityRelations(e, RELATION_PART_OF, { + const rows = entities.map(entity => { + const partOfSystemRelations = getEntityRelations(entity, RELATION_PART_OF, { kind: 'system', }); - const ownedByRelations = getEntityRelations(e, RELATION_OWNED_BY); + const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); return { - ...(e as ApiEntityV1alpha1), - row: { + entity: entity as ApiEntityV1alpha1, + resolved: { ownedByRelationsTitle: ownedByRelations .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) .join(', '), diff --git a/plugins/api-docs/src/components/ApisCards/ApisTable.tsx b/plugins/api-docs/src/components/ApisCards/ApisTable.tsx index 0595044327..30716a61fb 100644 --- a/plugins/api-docs/src/components/ApisCards/ApisTable.tsx +++ b/plugins/api-docs/src/components/ApisCards/ApisTable.tsx @@ -30,8 +30,9 @@ import { import React from 'react'; import { ApiTypeTitle } from '../ApiDefinitionCard'; -type EntityRow = ApiEntity & { - row: { +type EntityRow = { + entity: ApiEntity; + resolved: { partOfSystemRelationTitle?: string; partOfSystemRelations: EntityName[]; ownedByRelationsTitle?: string; @@ -42,44 +43,44 @@ type EntityRow = ApiEntity & { const columns: TableColumn[] = [ { title: 'Name', - field: 'metadata.name', + field: 'entity.metadata.name', highlight: true, - render: entity => ( + render: ({ entity }) => ( {entity.metadata.name} ), }, { title: 'System', - field: 'row.partOfSystemRelationTitle', - render: entity => ( + field: 'resolved.partOfSystemRelationTitle', + render: ({ resolved }) => ( ), }, { title: 'Owner', - field: 'row.ownedByRelationsTitle', - render: entity => ( + field: 'resolved.ownedByRelationsTitle', + render: ({ resolved }) => ( ), }, { title: 'Lifecycle', - field: 'spec.lifecycle', + field: 'entity.spec.lifecycle', }, { title: 'Type', - field: 'spec.type', - render: entity => , + field: 'entity.spec.type', + render: ({ entity }) => , }, { title: 'Description', - field: 'metadata.description', + field: 'entity.metadata.description', width: 'auto', }, ]; @@ -103,15 +104,19 @@ export const ApisTable = ({ entities, title, variant = 'gridItem' }: Props) => { const rows = entities // TODO: For now we skip all APIs that we can't find without a warning! .filter(e => e !== undefined) - .map(e => { - const partOfSystemRelations = getEntityRelations(e, RELATION_PART_OF, { - kind: 'system', - }); - const ownedByRelations = getEntityRelations(e, RELATION_OWNED_BY); + .map(entity => { + const partOfSystemRelations = getEntityRelations( + entity, + RELATION_PART_OF, + { + kind: 'system', + }, + ); + const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); return { - ...(e as ApiEntity), - row: { + entity: entity as ApiEntity, + resolved: { ownedByRelationsTitle: ownedByRelations .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) .join(', '), diff --git a/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx b/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx index 1ac82991eb..bd9b0c765f 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx @@ -29,8 +29,9 @@ import { } from '@backstage/plugin-catalog-react'; import React from 'react'; -type EntityRow = ComponentEntity & { - row: { +type EntityRow = { + entity: ComponentEntity; + resolved: { partOfSystemRelationTitle?: string; partOfSystemRelations: EntityName[]; ownedByRelationsTitle?: string; @@ -41,43 +42,43 @@ type EntityRow = ComponentEntity & { const columns: TableColumn[] = [ { title: 'Name', - field: 'metadata.name', + field: 'entity.metadata.name', highlight: true, - render: entity => ( + render: ({ entity }) => ( {entity.metadata.name} ), }, { title: 'System', - field: 'row.partOfSystemRelationTitle', - render: entity => ( + field: 'resolved.partOfSystemRelationTitle', + render: ({ resolved }) => ( ), }, { title: 'Owner', - field: 'row.ownedByRelationsTitle', - render: entity => ( + field: 'resolved.ownedByRelationsTitle', + render: ({ resolved }) => ( ), }, { title: 'Lifecycle', - field: 'spec.lifecycle', + field: 'entity.spec.lifecycle', }, { title: 'Type', - field: 'spec.type', + field: 'entity.spec.type', }, { title: 'Description', - field: 'metadata.description', + field: 'entity.metadata.description', width: 'auto', }, ]; @@ -106,15 +107,19 @@ export const ComponentsTable = ({ const rows = entities // TODO: For now we skip all Components that we can't find without a warning! .filter(e => e !== undefined) - .map(e => { - const partOfSystemRelations = getEntityRelations(e, RELATION_PART_OF, { - kind: 'system', - }); - const ownedByRelations = getEntityRelations(e, RELATION_OWNED_BY); + .map(entity => { + const partOfSystemRelations = getEntityRelations( + entity, + RELATION_PART_OF, + { + kind: 'system', + }, + ); + const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); return { - ...(e as ComponentEntity), - row: { + entity: entity as ComponentEntity, + resolved: { ownedByRelationsTitle: ownedByRelations .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) .join(', '), diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 6f91b250df..5fc4445053 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -39,8 +39,9 @@ import { favouriteEntityTooltip, } from '../FavouriteEntity/FavouriteEntity'; -type EntityRow = Entity & { - row: { +type EntityRow = { + entity: Entity; + resolved: { partOfSystemRelationTitle?: string; partOfSystemRelations: EntityName[]; ownedByRelationsTitle?: string; @@ -51,47 +52,47 @@ type EntityRow = Entity & { const columns: TableColumn[] = [ { title: 'Name', - field: 'metadata.name', + field: 'entity.metadata.name', highlight: true, - render: entity => ( + render: ({ entity }) => ( {entity.metadata.name} ), }, { title: 'System', - field: 'row.partOfSystemRelationTitle', - render: entity => ( + field: 'resolved.partOfSystemRelationTitle', + render: ({ resolved }) => ( ), }, { title: 'Owner', - field: 'row.ownedByRelationsTitle', - render: entity => ( + field: 'resolved.ownedByRelationsTitle', + render: ({ resolved }) => ( ), }, { title: 'Lifecycle', - field: 'spec.lifecycle', + field: 'entity.spec.lifecycle', }, { title: 'Description', - field: 'metadata.description', + field: 'entity.metadata.description', }, { title: 'Tags', - field: 'metadata.tags', + field: 'entity.metadata.tags', cellStyle: { padding: '0px 16px 0px 20px', }, - render: entity => ( + render: ({ entity }) => ( <> {entity.metadata.tags && entity.metadata.tags.map(t => ( @@ -133,9 +134,9 @@ export const CatalogTable = ({ ); } - const actions: TableProps['actions'] = [ - (rowData: Entity) => { - const location = findLocationForEntityMeta(rowData.metadata); + const actions: TableProps['actions'] = [ + ({ entity }) => { + const location = findLocationForEntityMeta(entity.metadata); return { icon: () => , tooltip: 'View', @@ -145,8 +146,8 @@ export const CatalogTable = ({ }, }; }, - (rowData: Entity) => { - const location = findLocationForEntityMeta(rowData.metadata); + ({ entity }) => { + const location = findLocationForEntityMeta(entity.metadata); return { icon: () => , tooltip: 'Edit', @@ -156,26 +157,26 @@ export const CatalogTable = ({ }, }; }, - (rowData: Entity) => { - const isStarred = isStarredEntity(rowData); + ({ entity }) => { + const isStarred = isStarredEntity(entity); return { cellStyle: { paddingLeft: '1em' }, icon: () => favouriteEntityIcon(isStarred), tooltip: favouriteEntityTooltip(isStarred), - onClick: () => toggleStarredEntity(rowData), + onClick: () => toggleStarredEntity(entity), }; }, ]; - const rows = entities.map(e => { - const partOfSystemRelations = getEntityRelations(e, RELATION_PART_OF, { + const rows = entities.map(entity => { + const partOfSystemRelations = getEntityRelations(entity, RELATION_PART_OF, { kind: 'system', }); - const ownedByRelations = getEntityRelations(e, RELATION_OWNED_BY); + const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); return { - ...e, - row: { + entity, + resolved: { ownedByRelationsTitle: ownedByRelations .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) .join(', '), From e3a11945eaa1fda3b29791d248d35228f17c028f Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Fri, 29 Jan 2021 17:34:51 -0500 Subject: [PATCH 84/99] Add repoVisibility for GitLab repos --- .../scaffolder/stages/publish/gitlab.test.ts | 26 ++++++++++++------- .../src/scaffolder/stages/publish/gitlab.ts | 20 +++++++++++--- .../scaffolder/stages/publish/publishers.ts | 19 ++++++++++---- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts index 368f1bb29f..61c8480efb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts @@ -54,11 +54,14 @@ describe('GitLab Publisher', () => { describe('publish: createRemoteInGitLab', () => { it('should use gitbeaker to create a repo in a namespace if the namespace property is set', async () => { - const publisher = await GitlabPublisher.fromConfig({ - host: 'gitlab.com', - token: 'fake-token', - baseUrl: 'https://gitlab.hosted.com', - }); + const publisher = await GitlabPublisher.fromConfig( + { + host: 'gitlab.com', + token: 'fake-token', + baseUrl: 'https://gitlab.hosted.com', + }, + { repoVisibility: 'public' }, + ); mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 42, @@ -88,6 +91,7 @@ describe('GitLab Publisher', () => { expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({ namespace_id: 42, name: 'test', + visibility: 'public', }); expect(initRepoAndPush).toHaveBeenCalledWith({ dir: resultPath, @@ -98,10 +102,13 @@ describe('GitLab Publisher', () => { }); it('should use gitbeaker to create a repo in the authed user if the namespace property is not set', async () => { - const publisher = await GitlabPublisher.fromConfig({ - host: 'gitlab.com', - token: 'fake-token', - }); + const publisher = await GitlabPublisher.fromConfig( + { + host: 'gitlab.com', + token: 'fake-token', + }, + { repoVisibility: 'public' }, + ); mockGitlabClient.Namespaces.show.mockResolvedValue({}); mockGitlabClient.Users.current.mockResolvedValue({ @@ -128,6 +135,7 @@ describe('GitLab Publisher', () => { expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({ namespace_id: 21, name: 'test', + visibility: 'public', }); expect(initRepoAndPush).toHaveBeenCalledWith({ dir: resultPath, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts index ace5791b1f..4b10d653da 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts @@ -22,18 +22,31 @@ import parseGitUrl from 'git-url-parse'; import path from 'path'; import { GitLabIntegrationConfig } from '@backstage/integration'; +export type RepoVisibilityOptions = 'private' | 'internal' | 'public'; + export class GitlabPublisher implements PublisherBase { - static async fromConfig(config: GitLabIntegrationConfig) { + static async fromConfig( + config: GitLabIntegrationConfig, + { repoVisibility }: { repoVisibility: RepoVisibilityOptions }, + ) { if (!config.token) { return undefined; } const client = new Gitlab({ host: config.baseUrl, token: config.token }); - return new GitlabPublisher({ token: config.token, client }); + return new GitlabPublisher({ + token: config.token, + client, + repoVisibility, + }); } constructor( - private readonly config: { token: string; client: GitlabClient }, + private readonly config: { + token: string; + client: GitlabClient; + repoVisibility: RepoVisibilityOptions; + }, ) {} async publish({ @@ -85,6 +98,7 @@ export class GitlabPublisher implements PublisherBase { const project = (await this.config.client.Projects.create({ namespace_id: targetNamespace, name: name, + visibility: this.config.repoVisibility, })) as { http_url_to_repo: string }; return project?.http_url_to_repo; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts index 79eb3b7708..ac8a87ef8d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts @@ -98,7 +98,13 @@ export class Publishers implements PublisherBuilder { } for (const integration of scm.gitlab.list()) { - const publisher = await GitlabPublisher.fromConfig(integration.config); + const repoVisibility = (config.getOptionalString( + 'scaffolder.gitlab.visibility', + ) ?? 'public') as RepoVisibilityOptions; + + const publisher = await GitlabPublisher.fromConfig(integration.config, { + repoVisibility, + }); if (publisher) { publishers.register(integration.config.host, publisher); @@ -107,10 +113,13 @@ export class Publishers implements PublisherBuilder { publishers.register( integration.config.host, - await GitlabPublisher.fromConfig({ - token: config.getOptionalString('scaffolder.gitlab.token') ?? '', - host: integration.config.host, - }), + await GitlabPublisher.fromConfig( + { + token: config.getOptionalString('scaffolder.gitlab.token') ?? '', + host: integration.config.host, + }, + { repoVisibility }, + ), ); } } From a4e526dfe38909c672fb870867f2c51de475902a Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sat, 30 Jan 2021 09:31:53 -0500 Subject: [PATCH 85/99] Bitbucket repo visibility --- .../stages/publish/bitbucket.test.ts | 24 +++++---- .../scaffolder/stages/publish/bitbucket.ts | 11 +++- .../scaffolder/stages/publish/publishers.ts | 52 +++++++++++++------ 3 files changed, 62 insertions(+), 25 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts index 419c81ea7e..4cf5e83ba3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts @@ -63,11 +63,14 @@ describe('Bitbucket Publisher', () => { ), ); - const publisher = await BitbucketPublisher.fromConfig({ - host: 'bitbucket.org', - username: 'fake-user', - appPassword: 'fake-token', - }); + const publisher = await BitbucketPublisher.fromConfig( + { + host: 'bitbucket.org', + username: 'fake-user', + appPassword: 'fake-token', + }, + { repoVisibility: 'private' }, + ); const result = await publisher.publish({ values: { @@ -122,10 +125,13 @@ describe('Bitbucket Publisher', () => { ), ); - const publisher = await BitbucketPublisher.fromConfig({ - host: 'bitbucket.mycompany.com', - token: 'fake-token', - }); + const publisher = await BitbucketPublisher.fromConfig( + { + host: 'bitbucket.mycompany.com', + token: 'fake-token', + }, + { repoVisibility: 'private' }, + ); const result = await publisher.publish({ values: { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts index 2a274144ff..373920c005 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts @@ -21,17 +21,23 @@ import { BitbucketIntegrationConfig } from '@backstage/integration'; import parseGitUrl from 'git-url-parse'; import path from 'path'; +export type RepoVisibilityOptions = 'private' | 'public'; + // TODO(blam): We should probably start to use a bitbucket client here that we can change // the baseURL to point at on-prem or public bitbucket versions like we do for // github and ghe. There's to much logic and not enough types here for us to say that this way is better than using // a supported bitbucket client if one exists. export class BitbucketPublisher implements PublisherBase { - static async fromConfig(config: BitbucketIntegrationConfig) { + static async fromConfig( + config: BitbucketIntegrationConfig, + { repoVisibility }: { repoVisibility: RepoVisibilityOptions }, + ) { return new BitbucketPublisher({ host: config.host, token: config.token, appPassword: config.appPassword, username: config.username, + repoVisibility, }); } @@ -41,6 +47,7 @@ export class BitbucketPublisher implements PublisherBase { token?: string; appPassword?: string; username?: string; + repoVisibility: RepoVisibilityOptions; }, ) {} @@ -101,6 +108,7 @@ export class BitbucketPublisher implements PublisherBase { body: JSON.stringify({ scm: 'git', description: description, + is_private: this.config.repoVisibility === 'private', }), headers: { Authorization: `Basic ${buffer.toString('base64')}`, @@ -144,6 +152,7 @@ export class BitbucketPublisher implements PublisherBase { body: JSON.stringify({ name: name, description: description, + is_private: this.config.repoVisibility === 'private', }), headers: { Authorization: `Bearer ${this.config.token}`, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts index ac8a87ef8d..30201e6dca 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts @@ -16,10 +16,19 @@ import { Config } from '@backstage/config'; import { PublisherBase, PublisherBuilder } from './types'; -import { GithubPublisher, RepoVisibilityOptions } from './github'; -import { GitlabPublisher } from './gitlab'; +import { + GithubPublisher, + RepoVisibilityOptions as GithubRepoVisibilityOptions, +} from './github'; +import { + GitlabPublisher, + RepoVisibilityOptions as GitlabRepoVisibilityOptions, +} from './gitlab'; import { AzurePublisher } from './azure'; -import { BitbucketPublisher } from './bitbucket'; +import { + BitbucketPublisher, + RepoVisibilityOptions as BitbucketRepoVisibilityOptions, +} from './bitbucket'; import { Logger } from 'winston'; import { ScmIntegrations } from '@backstage/integration'; @@ -74,7 +83,7 @@ export class Publishers implements PublisherBuilder { for (const integration of scm.github.list()) { const repoVisibility = (config.getOptionalString( 'scaffolder.github.visibility', - ) ?? 'public') as RepoVisibilityOptions; + ) ?? 'public') as GithubRepoVisibilityOptions; const publisher = await GithubPublisher.fromConfig(integration.config, { repoVisibility, @@ -100,7 +109,7 @@ export class Publishers implements PublisherBuilder { for (const integration of scm.gitlab.list()) { const repoVisibility = (config.getOptionalString( 'scaffolder.gitlab.visibility', - ) ?? 'public') as RepoVisibilityOptions; + ) ?? 'public') as GitlabRepoVisibilityOptions; const publisher = await GitlabPublisher.fromConfig(integration.config, { repoVisibility, @@ -125,7 +134,16 @@ export class Publishers implements PublisherBuilder { } for (const integration of scm.bitbucket.list()) { - const publisher = await BitbucketPublisher.fromConfig(integration.config); + const repoVisibility = (config.getOptionalString( + 'scaffolder.bitbucket.visibility', + ) ?? 'public') as BitbucketRepoVisibilityOptions; + + const publisher = await BitbucketPublisher.fromConfig( + integration.config, + { + repoVisibility, + }, + ); if (publisher) { publishers.register(integration.config.host, publisher); @@ -134,15 +152,19 @@ export class Publishers implements PublisherBuilder { publishers.register( integration.config.host, - await BitbucketPublisher.fromConfig({ - token: config.getOptionalString('scaffolder.bitbucket.token') ?? '', - username: - config.getOptionalString('scaffolder.bitbucket.username') ?? '', - appPassword: - config.getOptionalString('scaffolder.bitbucket.appPassword') ?? - '', - host: integration.config.host, - }), + await BitbucketPublisher.fromConfig( + { + token: + config.getOptionalString('scaffolder.bitbucket.token') ?? '', + username: + config.getOptionalString('scaffolder.bitbucket.username') ?? '', + appPassword: + config.getOptionalString('scaffolder.bitbucket.appPassword') ?? + '', + host: integration.config.host, + }, + { repoVisibility }, + ), ); } } From 2d56971596819d7160d881b1b65502f3d14c7f0a Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sat, 30 Jan 2021 10:25:41 -0500 Subject: [PATCH 86/99] add scaffolder visibility config for GitLab and BitBucket --- app-config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app-config.yaml b/app-config.yaml index f71649659b..763dd0c258 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -243,6 +243,7 @@ scaffolder: baseUrl: https://gitlab.com token: $env: GITLAB_TOKEN + visibility: public # or 'internal' or 'private' azure: baseUrl: https://dev.azure.com/{your-organization} api: @@ -255,6 +256,7 @@ scaffolder: $env: BITBUCKET_USERNAME token: $env: BITBUCKET_TOKEN + visibility: public # or or 'private' auth: environment: development From 529d16d278f3996a517d55d8be3fafa2f84063df Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sat, 30 Jan 2021 10:26:26 -0500 Subject: [PATCH 87/99] changeset --- .changeset/sour-gorillas-fail.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .changeset/sour-gorillas-fail.md diff --git a/.changeset/sour-gorillas-fail.md b/.changeset/sour-gorillas-fail.md new file mode 100644 index 0000000000..c6394ed76e --- /dev/null +++ b/.changeset/sour-gorillas-fail.md @@ -0,0 +1,23 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +# Repo visibility for GitLab and BitBucket repos + +**NOTE: This changes default repo visibility from `private` to `public` for GitLab and BitBucket** which +is consistent with the GitHub default. If you were counting on `private` visibility, you'll need to update +your scaffolder config to use `private`. + +This adds repo visibility feature parity with GitHub for GitLab and BitBucket. + +To configure the repo visibility, set scaffolder._type_.visibility as in this example: + +```yaml +scaffolder: + github: + visibility: private # 'public' or 'internal' or 'private' (default is 'public') + gitlab: + visibility: public # 'public' or 'internal' or 'private' (default is 'public') + bitbucket: + visibility: public # 'public' or 'private' (default is 'public') +``` From c3609ebb44f8a28cca7d6696743e824960e67b31 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Mon, 1 Feb 2021 10:26:33 -0500 Subject: [PATCH 88/99] add visibility to schema --- plugins/scaffolder-backend/config.d.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/scaffolder-backend/config.d.ts b/plugins/scaffolder-backend/config.d.ts index 28e2515471..55ed959edc 100644 --- a/plugins/scaffolder-backend/config.d.ts +++ b/plugins/scaffolder-backend/config.d.ts @@ -19,9 +19,11 @@ export interface Config { scaffolder?: { github?: { [key: string]: string; + visiblity?: string; }; gitlab?: { api: { [key: string]: string }; + visiblity?: string; }; azure?: { baseUrl: string; @@ -29,6 +31,7 @@ export interface Config { }; bitbucket?: { api: { [key: string]: string }; + visiblity?: string; }; }; } From 77fe4ef3cd107bd9ffc7acb505d1bf2dd4631747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 1 Feb 2021 17:09:03 +0100 Subject: [PATCH 89/99] docs: add a miniature article about observability --- docs/plugins/observability.md | 21 +++++++++++++++++++++ microsite/sidebars.json | 3 ++- mkdocs.yml | 1 + 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 docs/plugins/observability.md diff --git a/docs/plugins/observability.md b/docs/plugins/observability.md new file mode 100644 index 0000000000..f52dd130bd --- /dev/null +++ b/docs/plugins/observability.md @@ -0,0 +1,21 @@ +--- +id: observability +title: Observability +# prettier-ignore +description: Adding Observability to Your Plugin +--- + +This article briefly describes the observability option that are available to a +Backstage integrator. + +## Google Analytics + +There is a basic Google Analytics integration built into Backstage. You can +enable it by adding the following to your app configuration: + +```yaml +app: + googleAnalyticsTrackingId: UA-000000-0 +``` + +Replace the tracking ID with your own. diff --git a/microsite/sidebars.json b/microsite/sidebars.json index f855327627..ebce8d900c 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -129,7 +129,8 @@ "ids": [ "plugins/publishing", "plugins/publish-private", - "plugins/add-to-marketplace" + "plugins/add-to-marketplace", + "plugins/observability" ] } ], diff --git a/mkdocs.yml b/mkdocs.yml index 6236d1c1a0..a303fd9978 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -80,6 +80,7 @@ nav: - Publishing: - Open source and npm: 'plugins/publishing.md' - Private/internal (non-open source): 'plugins/publish-private.md' + - Observability: 'plugins/observability.md' - Configuration: - Overview: 'conf/index.md' - Reading Configuration: 'conf/reading.md' From 885e695803cf113571313d368911ac114e39e1ce Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 1 Feb 2021 17:28:36 +0100 Subject: [PATCH 90/99] Display the namespace of an entity if required --- .../src/components/ApiExplorerTable/ApiExplorerTable.tsx | 8 ++++++-- .../catalog/src/components/CatalogTable/CatalogTable.tsx | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx index 6baacf5961..101c28332a 100644 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx +++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx @@ -42,6 +42,7 @@ import { ApiTypeTitle } from '../ApiDefinitionCard'; type EntityRow = { entity: ApiEntityV1alpha1; resolved: { + name: string; partOfSystemRelationTitle?: string; partOfSystemRelations: EntityName[]; ownedByRelationsTitle?: string; @@ -52,10 +53,10 @@ type EntityRow = { const columns: TableColumn[] = [ { title: 'Name', - field: 'entity.metadata.name', + field: 'resolved.name', highlight: true, render: ({ entity }) => ( - {entity.metadata.name} + ), }, { @@ -167,6 +168,9 @@ export const ApiExplorerTable = ({ return { entity: entity as ApiEntityV1alpha1, resolved: { + name: formatEntityRefTitle(entity, { + defaultKind: 'API', + }), ownedByRelationsTitle: ownedByRelations .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) .join(', '), diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 5fc4445053..30a19145f2 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -42,6 +42,7 @@ import { type EntityRow = { entity: Entity; resolved: { + name: string; partOfSystemRelationTitle?: string; partOfSystemRelations: EntityName[]; ownedByRelationsTitle?: string; @@ -52,10 +53,10 @@ type EntityRow = { const columns: TableColumn[] = [ { title: 'Name', - field: 'entity.metadata.name', + field: 'resolved.name', highlight: true, render: ({ entity }) => ( - {entity.metadata.name} + ), }, { @@ -177,6 +178,9 @@ export const CatalogTable = ({ return { entity, resolved: { + name: formatEntityRefTitle(entity, { + defaultKind: 'Component', + }), ownedByRelationsTitle: ownedByRelations .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) .join(', '), From 2bf3ad4ce56f6ddecc20e6d2386be44d1485d658 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Mon, 1 Feb 2021 11:45:38 -0500 Subject: [PATCH 91/99] better document the config options --- plugins/scaffolder-backend/config.d.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend/config.d.ts b/plugins/scaffolder-backend/config.d.ts index 55ed959edc..3b31b4f951 100644 --- a/plugins/scaffolder-backend/config.d.ts +++ b/plugins/scaffolder-backend/config.d.ts @@ -19,11 +19,17 @@ export interface Config { scaffolder?: { github?: { [key: string]: string; - visiblity?: string; + /** + * The visibility to set on created repositories. + */ + visiblity?: 'public' | 'internal' | 'private'; }; gitlab?: { api: { [key: string]: string }; - visiblity?: string; + /** + * The visibility to set on created repositories. + */ + visiblity?: 'public' | 'internal' | 'private'; }; azure?: { baseUrl: string; @@ -31,7 +37,10 @@ export interface Config { }; bitbucket?: { api: { [key: string]: string }; - visiblity?: string; + /** + * The visibility to set on created repositories. + */ + visiblity?: 'public' | 'private'; }; }; } From 5b2dd00f55312f944538c72424ea32c17f42b308 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Mon, 1 Feb 2021 21:51:03 -0500 Subject: [PATCH 92/99] Fix typos --- .../user-settings/src/components/FeatureFlags/EmptyFlags.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/user-settings/src/components/FeatureFlags/EmptyFlags.tsx b/plugins/user-settings/src/components/FeatureFlags/EmptyFlags.tsx index 5df4df4450..de6f4703e0 100644 --- a/plugins/user-settings/src/components/FeatureFlags/EmptyFlags.tsx +++ b/plugins/user-settings/src/components/FeatureFlags/EmptyFlags.tsx @@ -32,11 +32,11 @@ export const EmptyFlags = () => ( - An example how how to add a feature flags is highlighted below: + An example for how to add a feature flag is highlighted below: Date: Mon, 1 Feb 2021 21:51:32 -0500 Subject: [PATCH 93/99] Remove extraneous grid --- .../src/components/General/Profile.tsx | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/plugins/user-settings/src/components/General/Profile.tsx b/plugins/user-settings/src/components/General/Profile.tsx index b0231d95b6..c8034085bd 100644 --- a/plugins/user-settings/src/components/General/Profile.tsx +++ b/plugins/user-settings/src/components/General/Profile.tsx @@ -24,31 +24,27 @@ export const Profile = () => { const { profile, displayName } = useUserProfile(); return ( - - - - - - - - - - - - {displayName} - - - {profile.email} - - - - - - + + + + + + + + + + {displayName} + + + {profile.email} + - + + + + - + ); }; From c88aa5f04aa5b18cd60660cdd65db437abfddb15 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Mon, 1 Feb 2021 21:52:06 -0500 Subject: [PATCH 94/99] Keep PinButton visible --- plugins/user-settings/src/components/General/General.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/user-settings/src/components/General/General.tsx b/plugins/user-settings/src/components/General/General.tsx index 2d30c00eec..b5054217e4 100644 --- a/plugins/user-settings/src/components/General/General.tsx +++ b/plugins/user-settings/src/components/General/General.tsx @@ -14,15 +14,13 @@ * limitations under the License. */ import { InfoCard } from '@backstage/core'; -import { Grid, List, useMediaQuery, useTheme } from '@material-ui/core'; +import { Grid, List } from '@material-ui/core'; import React from 'react'; import { PinButton } from './PinButton'; import { Profile } from './Profile'; import { ThemeToggle } from './ThemeToggle'; export const General = () => { - const theme = useTheme(); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); return ( @@ -32,7 +30,7 @@ export const General = () => { - {!fullScreen && } + From de98c32ed40d96c095f03f09867713e39cd3aef2 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Mon, 1 Feb 2021 21:53:37 -0500 Subject: [PATCH 95/99] Add changeset --- .changeset/tidy-news-perform.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tidy-news-perform.md diff --git a/.changeset/tidy-news-perform.md b/.changeset/tidy-news-perform.md new file mode 100644 index 0000000000..055f051da7 --- /dev/null +++ b/.changeset/tidy-news-perform.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': patch +--- + +Keep the Pin Sidebar setting visible on small screens. From 03144fb7b51b55c16f3ce48680fe2a6a09a90aa5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Feb 2021 04:47:52 +0000 Subject: [PATCH 96/99] chore(deps): bump jenkins from 0.28.0 to 0.28.1 Bumps [jenkins](https://github.com/silas/node-jenkins) from 0.28.0 to 0.28.1. - [Release notes](https://github.com/silas/node-jenkins/releases) - [Commits](https://github.com/silas/node-jenkins/compare/0.28.0...0.28.1) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2ac7dffa6a..783d2c793c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16438,9 +16438,9 @@ jake@^10.6.1: minimatch "^3.0.4" jenkins@^0.28.0: - version "0.28.0" - resolved "https://registry.npmjs.org/jenkins/-/jenkins-0.28.0.tgz#72d6fcc452145403b34f6d4ecbd877ee1ab77fca" - integrity sha512-EGzzZcyFwXBCPZZoDNvZTPmZOqaHALfAStNfCF37oh+Mc6G/e0MwIuQjx5kMEynTXR9bF5EwLiuMTiTf5kHk5g== + version "0.28.1" + resolved "https://registry.npmjs.org/jenkins/-/jenkins-0.28.1.tgz#f7951798ee5d2bb501a831979b6b5ecc1a922a64" + integrity sha512-gcC4QUrP4VzdqOMHoVzh36XlJprxJkI2HGLQSY7w84KoCTVNDcR/8O00tYyXp9vrZOx4wl5zCXLVKMgH2IoyJQ== dependencies: papi "^0.29.0" From b3058e29915d028cc35f63405daceabd1068356a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Feb 2021 04:55:58 +0000 Subject: [PATCH 97/99] chore(deps-dev): bump @types/express-serve-static-core Bumps [@types/express-serve-static-core](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/express-serve-static-core) from 4.17.9 to 4.17.18. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/express-serve-static-core) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2ac7dffa6a..625dad9208 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6654,9 +6654,9 @@ integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.5": - version "4.17.9" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.9.tgz#2d7b34dcfd25ec663c25c85d76608f8b249667f1" - integrity sha512-DG0BYg6yO+ePW+XoDENYz8zhNGC3jDDEpComMYn7WJc4mY1Us8Rw9ax2YhJXxpyk2SF47PQAoQ0YyVT1a0bEkA== + version "4.17.18" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.18.tgz#8371e260f40e0e1ca0c116a9afcd9426fa094c40" + integrity sha512-m4JTwx5RUBNZvky/JJ8swEJPKFd8si08pPF2PfizYjGZOKr/svUWPcoUmLow6MmPzhasphB7gSTINY67xn3JNA== dependencies: "@types/node" "*" "@types/qs" "*" From f573c572fbd67c60b7a0932dc586b9131a50acd8 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Tue, 2 Feb 2021 10:28:42 +0100 Subject: [PATCH 98/99] microsite/docs: Scroll nav sidebar to view for loaded page --- microsite/siteConfig.js | 1 + .../static/js/scroll-nav-to-view-in-docs.js | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 microsite/static/js/scroll-nav-to-view-in-docs.js diff --git a/microsite/siteConfig.js b/microsite/siteConfig.js index 8d0c4ea57c..51354343d8 100644 --- a/microsite/siteConfig.js +++ b/microsite/siteConfig.js @@ -83,6 +83,7 @@ const siteConfig = { 'https://unpkg.com/medium-zoom@1.0.6/dist/medium-zoom.min.js', '/js/medium-zoom.js', '/js/dismissable-banner.js', + '/js/scroll-nav-to-view-in-docs.js', ], // On page navigation for the current documentation page. diff --git a/microsite/static/js/scroll-nav-to-view-in-docs.js b/microsite/static/js/scroll-nav-to-view-in-docs.js new file mode 100644 index 0000000000..0e4da26674 --- /dev/null +++ b/microsite/static/js/scroll-nav-to-view-in-docs.js @@ -0,0 +1,23 @@ +// On backstage.io/docs pages, scroll the Nav sidebar to focus on +// the page being viewed. Helpful when the Nav is large enough that +// the selected page is hidden somewhere at bottom. +// Credits: https://github.com/facebook/docusaurus/issues/823#issuecomment-421152269 +document.addEventListener('DOMContentLoaded', () => { + // Find the active nav item in the sidebar + const item = document.getElementsByClassName('navListItemActive')[0]; + if (!item) { + return; + } + const bounding = item.getBoundingClientRect(); + if ( + bounding.top >= 0 && + bounding.bottom <= + (window.innerHeight || document.documentElement.clientHeight) + ) { + // Already visible. Do nothing. + } else { + // Not visible. Scroll sidebar. + item.scrollIntoView({ block: 'start', inline: 'nearest' }); + document.body.scrollTop = document.documentElement.scrollTop = 0; + } +}); From 246c9c6edc82fb6f370756cd8bf5d92be59ff960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 1 Feb 2021 19:06:30 +0100 Subject: [PATCH 99/99] Update docs/plugins/observability.md Co-authored-by: Adam Harvey --- .github/styles/vocab.txt | 2 ++ docs/plugins/observability.md | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index d545b380f5..2991a6babd 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -218,6 +218,7 @@ Spotify sqlite squidfunk src +stdout stefanalund subkey subtree @@ -258,6 +259,7 @@ Voi Wealthsimple Weaveworks Webpack +winston www WWW xyz diff --git a/docs/plugins/observability.md b/docs/plugins/observability.md index f52dd130bd..8b6668606f 100644 --- a/docs/plugins/observability.md +++ b/docs/plugins/observability.md @@ -5,7 +5,7 @@ title: Observability description: Adding Observability to Your Plugin --- -This article briefly describes the observability option that are available to a +This article briefly describes the observability options that are available to a Backstage integrator. ## Google Analytics @@ -19,3 +19,36 @@ app: ``` Replace the tracking ID with your own. + +For more information, learn about Google Analytics +[here](https://marketingplatform.google.com/about/analytics/). + +## Logging + +The backend supplies a central [winston](https://github.com/winstonjs/winston) +root logger that plugins are expected to use for their logging needs. In the +default production setup, it emits structured JSON logs on stdout, with a field +`"service": "backstage"` and also tagged on a per-plugin basis. Plugins that +want to more finely specify what part of their processes that emitted the log +message should add a `"component"` field to do so. + +An example log line could look as follows: + +```json +{ + "service": "backstage", + "type": "plugin", + "plugin": "catalog", + "component": "catalog-all-locations-refresh", + "level": "info", + "message": "Locations Refresh: Refreshing location bootstrap:bootstrap" +} +``` + +## Health Checks + +The example backend in the Backstage repository +[supplies](https://github.com/backstage/backstage/blob/bc18571b7a742863a770b2a54e785d6bbef7e184/packages/backend/src/index.ts#L99) +a very basic health check endpoint on the `/healthcheck` route. You may add such +a handler to your backend as well, and supply your own logic to it that fits +your particular health checking needs.