From 349c15d9f713c2a3c34bd8a9df7343b94ce3bb87 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Thu, 22 Apr 2021 15:01:40 -0400 Subject: [PATCH 01/18] make ratio optional Signed-off-by: Ryan Vazquez --- .../components/CostGrowth/CostGrowth.test.tsx | 6 + .../src/components/CostGrowth/CostGrowth.tsx | 61 +++++-- .../CostGrowth/CostGrowthIndicator.test.tsx | 9 +- .../CostGrowth/CostGrowthIndicator.tsx | 34 ++-- .../CostOverviewLegend.test.tsx | 153 ++++++++++++++++++ .../CostOverviewCard/CostOverviewLegend.tsx | 27 ++-- .../ProductEntityTable.tsx | 36 +++-- .../ProductInsightsChart.tsx | 33 ++-- .../cost-insights/src/testUtils/mockData.ts | 5 - .../cost-insights/src/testUtils/testUtils.ts | 12 +- .../src/types/ChangeStatistic.ts | 4 +- plugins/cost-insights/src/utils/assert.ts | 2 +- .../cost-insights/src/utils/change.test.ts | 16 +- plugins/cost-insights/src/utils/change.ts | 41 +++-- plugins/cost-insights/src/utils/formatters.ts | 10 +- 15 files changed, 336 insertions(+), 113 deletions(-) create mode 100644 plugins/cost-insights/src/components/CostOverviewCard/CostOverviewLegend.test.tsx diff --git a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx index f8c06845c9..7133ae81e3 100644 --- a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx +++ b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx @@ -46,6 +46,7 @@ describe.each` engineerCost | ratio | amount | expected ${200_000} | ${0} | ${0} | ${'Negligible'} ${200_000} | ${0} | ${8_333} | ${'Negligible'} + ${200_000} | ${undefined} | ${10_000} | ${`~1 ${engineers.unit}`} ${200_000} | ${0.000000001} | ${8_334} | ${`0% or ~1 ${engineers.unit}`} ${200_000} | ${-0.000000001} | ${10_000} | ${`0% or ~1 ${engineers.unit}`} ${200_000} | ${-0.8} | ${10_000} | ${`80% or ~1 ${engineers.unit}`} @@ -65,6 +66,9 @@ describe.each` engineerCost | ratio | amount | expected ${200_000} | ${0} | ${0} | ${'Negligible'} ${200_000} | ${0} | ${8_333} | ${'Negligible'} + ${200_000} | ${undefined} | ${-1_000} | ${'Negligible'} + ${200_000} | ${undefined} | ${1_000} | ${'Negligible'} + ${200_000} | ${undefined} | ${10_000} | ${'~$10,000'} ${200_000} | ${0.000000001} | ${8_334} | ${'0% or ~$8,334'} ${200_000} | ${-0.000000001} | ${10_000} | ${'0% or ~$10,000'} ${200_000} | ${-0.8} | ${10_000} | ${'80% or ~$10,000'} @@ -84,6 +88,8 @@ describe.each` engineerCost | ratio | amount | expected ${200_000} | ${0} | ${0} | ${'Negligible'} ${200_000} | ${0} | ${8_333} | ${'Negligible'} + ${200_000} | ${undefined} | ${1_000} | ${'Negligible'} + ${200_000} | ${undefined} | ${10_000} | ${`~2,857 ${carbon.unit}s`} ${200_000} | ${0.000000001} | ${8_334} | ${`0% or ~2,381 ${carbon.unit}s`} ${200_000} | ${-0.000000001} | ${10_000} | ${`0% or ~2,857 ${carbon.unit}s`} ${200_000} | ${-0.8} | ${10_000} | ${`80% or ~2,857 ${carbon.unit}s`} diff --git a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx index 01a0874636..196f8bec44 100644 --- a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx +++ b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx @@ -29,6 +29,7 @@ import { useCostGrowthStyles as useStyles } from '../../utils/styles'; import { formatPercent, formatCurrency } from '../../utils/formatters'; import { indefiniteArticleOf } from '../../utils/grammar'; import { useConfig, useCurrency } from '../../hooks'; +import { notEmpty } from '../../utils/assert'; export type CostGrowthProps = { change: ChangeStatistic; @@ -42,31 +43,65 @@ export const CostGrowth = ({ change, duration }: CostGrowthProps) => { // Only display costs in absolute values const amount = Math.abs(change.amount); - const ratio = Math.abs(change.ratio); + const ratio = Math.abs(change.ratio ?? NaN); const rate = rateOf(engineerCost, duration); const engineers = amount / rate; const converted = amount / (currency.rate ?? rate); + // If a ratio cannot be calculated, don't format. + const growth = notEmpty(change.ratio) + ? growthOf({ ratio: change.ratio, amount: engineers }) + : null; // Determine if growth is significant enough to highlight - const growth = growthOf(change.ratio, engineers); const classes = classnames({ [styles.excess]: growth === GrowthType.Excess, [styles.savings]: growth === GrowthType.Savings, }); - const percent = formatPercent(ratio); - - let cost = `${percent} or ~${formatCurrency(converted, currency.unit)}`; - // Always display the converted value but use the cost in engineers - // to determine negligibility, as costs should be time-period aware if (engineers < EngineerThreshold) { - cost = 'Negligible'; - } else if (currency.kind === CurrencyType.USD) { - cost = `${percent} or ~${currency.prefix}${formatCurrency(converted)}`; - } else if (amount < 1) { - cost = `less than ${indefiniteArticleOf(['a', 'an'], currency.unit)}`; + return Negligible; } - return {cost}; + if (currency.kind === CurrencyType.USD) { + // Do not display percentage if ratio cannot be calculated + if (isNaN(ratio)) { + return ( + + ~{currency.prefix} + {formatCurrency(converted)} + + ); + } + + return ( + + {formatPercent(ratio)} or ~{currency.prefix} + {formatCurrency(converted)} + + ); + } + + if (amount < 1) { + return ( + + less than {indefiniteArticleOf(['a', 'an'], currency.unit)} + + ); + } + + // Do not display percentage if ratio cannot be calculated + if (isNaN(ratio)) { + return ( + + ~{formatCurrency(converted, currency.unit)} + + ); + } + + return ( + + {formatPercent(ratio)} or ~{formatCurrency(converted, currency.unit)} + + ); }; diff --git a/plugins/cost-insights/src/components/CostGrowth/CostGrowthIndicator.test.tsx b/plugins/cost-insights/src/components/CostGrowth/CostGrowthIndicator.test.tsx index 8be5e238cc..6dc6f2cc93 100644 --- a/plugins/cost-insights/src/components/CostGrowth/CostGrowthIndicator.test.tsx +++ b/plugins/cost-insights/src/components/CostGrowth/CostGrowthIndicator.test.tsx @@ -21,8 +21,6 @@ import { ChangeThreshold, EngineerThreshold } from '../../types'; describe.each` ratio | amount | ariaLabel - ${-0.1} | ${undefined} | ${'savings'} - ${0.01} | ${undefined} | ${'excess'} ${ChangeThreshold.lower} | ${EngineerThreshold} | ${'savings'} ${ChangeThreshold.lower - 0.01} | ${EngineerThreshold} | ${'savings'} ${ChangeThreshold.lower - 0.01} | ${EngineerThreshold + 0.1} | ${'savings'} @@ -32,7 +30,7 @@ describe.each` `('growthOf', ({ ratio, amount, ariaLabel }) => { it(`should display the correct indicator for ${ariaLabel}`, async () => { const { getByLabelText } = await renderInTestApp( - , + , ); expect(getByLabelText(ariaLabel)).toBeInTheDocument(); }); @@ -40,7 +38,8 @@ describe.each` describe.each` ratio | amount - ${0} | ${undefined} + ${undefined} | ${0} + ${0} | ${0} ${ChangeThreshold.lower} | ${0} ${ChangeThreshold.lower + 0.01} | ${EngineerThreshold} ${ChangeThreshold.lower + 0.01} | ${EngineerThreshold + 0.1} @@ -49,7 +48,7 @@ describe.each` `('growthOf', ({ ratio, amount }) => { it('should display the correct indicator for negligible growth', async () => { const { queryByLabelText } = await renderInTestApp( - , + , ); expect(queryByLabelText('savings')).not.toBeInTheDocument(); expect(queryByLabelText('excess')).not.toBeInTheDocument(); diff --git a/plugins/cost-insights/src/components/CostGrowth/CostGrowthIndicator.tsx b/plugins/cost-insights/src/components/CostGrowth/CostGrowthIndicator.tsx index e220fdb8e7..a1c200c6b3 100644 --- a/plugins/cost-insights/src/components/CostGrowth/CostGrowthIndicator.tsx +++ b/plugins/cost-insights/src/components/CostGrowth/CostGrowthIndicator.tsx @@ -20,49 +20,33 @@ import { Typography, TypographyProps } from '@material-ui/core'; import { default as ArrowDropUp } from '@material-ui/icons/ArrowDropUp'; import { default as ArrowDropDown } from '@material-ui/icons/ArrowDropDown'; import { growthOf } from '../../utils/change'; -import { GrowthType } from '../../types'; +import { ChangeStatistic, GrowthType, Maybe } from '../../types'; import { useCostGrowthStyles as useStyles } from '../../utils/styles'; export type CostGrowthIndicatorProps = TypographyProps & { - ratio: number; - amount?: number; - formatter?: (amount: number) => string; + change: ChangeStatistic; + formatter?: (change: ChangeStatistic) => Maybe; }; export const CostGrowthIndicator = ({ - ratio, - amount, + change, formatter, className, ...props }: CostGrowthIndicatorProps) => { const classes = useStyles(); - const growth = growthOf(ratio, amount); + const growth = growthOf(change); const classNames = classnames(classes.indicator, className, { - [classes.savings]: growth === GrowthType.Savings, [classes.excess]: growth === GrowthType.Excess, + [classes.savings]: growth === GrowthType.Savings, }); - // Display cost as a factor of engineer cost growth and percentage growth - if (typeof amount === 'number') { - return ( - - {formatter ? formatter(amount) : amount} - {growth === GrowthType.Savings && ( - - )} - {growth === GrowthType.Excess && } - - ); - } - - // Display cost as a factor of percent change return ( - {formatter ? formatter(ratio) : ratio} - {ratio < 0 && } - {ratio > 0 && } + {formatter ? formatter(change) : change.ratio} + {growth === GrowthType.Excess && } + {growth === GrowthType.Savings && } ); }; diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewLegend.test.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewLegend.test.tsx new file mode 100644 index 0000000000..112081fed4 --- /dev/null +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewLegend.test.tsx @@ -0,0 +1,153 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { render } from '@testing-library/react'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { CostOverviewLegend } from './CostOverviewLegend'; +import { + MockBillingDateProvider, + MockConfigProvider, + MockFilterProvider, + MockCurrencyProvider, +} from '../../testUtils'; + +function renderInTestApp(children: JSX.Element) { + return render( + wrapInTestApp( + + + + {children} + + + , + ), + ); +} + +describe('', () => { + it('displays the legend without exploding', async () => { + const { findByText } = renderInTestApp( + , + ); + + expect(await findByText('Cost Trend')).toBeInTheDocument(); + expect(await findByText('MSC Trend')).toBeInTheDocument(); + }); + + it('does not display metric legend if metric data is not provided', async () => { + const { findByText, queryByText } = renderInTestApp( + , + ); + + expect(await findByText('Cost Trend')).toBeInTheDocument(); + expect(queryByText('MSC Trend')).not.toBeInTheDocument(); + }); +}); + +describe.each` + ratio | amount | title | expected + ${undefined} | ${1_000} | ${'∞'} | ${'Your Excess'} + ${undefined} | ${-1_000} | ${'-∞'} | ${'Your Savings'} +`('', ({ ratio, amount, title, expected }) => { + it('displays the correct legend if ratio cannot be calculated and costs are within time period', async () => { + const { findByText, findAllByText } = renderInTestApp( + , + ); + + expect(await findByText('Cost Trend')).toBeInTheDocument(); + expect(await findByText('MSC Trend')).toBeInTheDocument(); + expect(await findAllByText(title).then(res => res.length)).toBe(2); + expect(await findByText(expected)).toBeInTheDocument(); + }); +}); diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewLegend.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewLegend.tsx index ebba5173a9..0033ba4f14 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewLegend.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewLegend.tsx @@ -25,11 +25,13 @@ import { Metric, } from '../../types'; import { useLastCompleteBillingDate, useFilters } from '../../hooks'; -import { getComparedChange } from '../../utils/change'; +import { getComparedChange, choose } from '../../utils/change'; import { mapFiltersToProps } from './selector'; -import { formatPercent } from '../../utils/formatters'; +import { formatChange } from '../../utils/formatters'; import { CostGrowth } from '../CostGrowth'; +import { notEmpty } from '../../utils/assert'; + type CostOverviewLegendProps = { metric: Maybe; metricData: Maybe; @@ -42,9 +44,8 @@ export const CostOverviewLegend = ({ metricData, }: PropsWithChildren) => { const theme = useTheme(); - - const lastCompleteBillingDate = useLastCompleteBillingDate(); const { duration } = useFilters(mapFiltersToProps); + const lastCompleteBillingDate = useLastCompleteBillingDate(); const comparedChange = metricData ? getComparedChange( @@ -57,23 +58,25 @@ export const CostOverviewLegend = ({ return ( - - - {formatPercent(dailyCostData.change!.ratio)} - - - {metric && metricData && comparedChange && ( + {dailyCostData.change && ( + + + {formatChange(dailyCostData.change)} + + + )} + {metricData && metric && comparedChange && ( <> - {formatPercent(metricData.change.ratio)} + {formatChange(metricData.change)} diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx index 45576bda65..a8211fbbed 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx @@ -18,10 +18,11 @@ import React from 'react'; import classnames from 'classnames'; import { Table, TableColumn } from '@backstage/core'; import { Typography } from '@material-ui/core'; -import { costFormatter, formatPercent } from '../../utils/formatters'; +import { costFormatter, formatChange } from '../../utils/formatters'; import { useEntityDialogStyles as useStyles } from '../../utils/styles'; import { CostGrowthIndicator } from '../CostGrowth'; -import { BarChartOptions, Entity } from '../../types'; +import { BarChartOptions, ChangeStatistic, Entity } from '../../types'; +import { isUndefined } from '../../utils/assert'; export type ProductEntityTableOptions = Partial< Pick @@ -32,7 +33,7 @@ type RowData = { label: string; previous: number; current: number; - ratio: number; + change: ChangeStatistic; }; function createRenderer(col: keyof RowData, classes: Record) { @@ -41,7 +42,7 @@ function createRenderer(col: keyof RowData, classes: Record) { const rowStyles = classnames(classes.row, { [classes.rowTotal]: row.id === 'total', [classes.colFirst]: col === 'label', - [classes.colLast]: col === 'ratio', + [classes.colLast]: col === 'change', }); switch (col) { @@ -52,12 +53,12 @@ function createRenderer(col: keyof RowData, classes: Record) { {costFormatter.format(row[col])} ); - case 'ratio': + case 'change': return ( formatPercent(Math.abs(amount))} + change={row.change} + formatter={formatChange} /> ); default: @@ -75,10 +76,15 @@ function createSorter(field?: keyof Omit) { if (a.id === 'total') return 1; if (b.id === 'total') return 1; if (field === 'label') return a.label.localeCompare(b.label); + if (field === 'change') { + if (formatChange(a[field]) === '∞' || formatChange(b[field]) === '-∞') + return 1; + if (formatChange(a[field]) === '-∞' || formatChange(b[field]) === '∞') + return -1; + return a[field].ratio! - b[field].ratio!; + } - return field - ? a[field] - b[field] - : b.previous + b.current - (a.previous + a.current); + return b.previous + b.current - (a.previous + a.current); }; } @@ -134,11 +140,11 @@ export const ProductEntityTable = ({ customSort: createSorter('current'), }, { - field: 'ratio', + field: 'change', title: Change, align: 'right', - render: createRenderer('ratio', classes), - customSort: createSorter('ratio'), + render: createRenderer('change', classes), + customSort: createSorter('change'), }, ]; @@ -148,14 +154,14 @@ export const ProductEntityTable = ({ label: e.id || 'Unknown', previous: e.aggregation[0], current: e.aggregation[1], - ratio: e.change.ratio, + change: e.change, })) .concat({ id: 'total', label: 'Total', previous: entity.aggregation[0], current: entity.aggregation[1], - ratio: entity.change.ratio, + change: entity.change, }) .sort(createSorter()); diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx index ace2766277..07fea77d9d 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx @@ -40,7 +40,7 @@ import { findAnyKey, assertAlways, } from '../../utils/assert'; -import { formatPeriod, formatPercent } from '../../utils/formatters'; +import { formatPeriod, formatChange } from '../../utils/formatters'; import { titleOf, tooltipItemOf, @@ -54,6 +54,7 @@ import { useBarChartLayoutStyles as useLayoutStyles, } from '../../utils/styles'; import { Duration, Entity, Maybe } from '../../types'; +import { choose } from '../../utils/change'; export type ProductInsightsChartProps = { billingDate: string; @@ -86,7 +87,6 @@ export const ProductInsightsChart = ({ return breakdowns.length > 0; }, [entities, activeLabel]); - const legendTitle = `Cost ${entity.change.ratio <= 0 ? 'Savings' : 'Growth'}`; const costStart = entity.aggregation[0]; const costEnd = entity.aggregation[1]; const resources = entities.map(resourceOf); @@ -136,7 +136,6 @@ export const ProductInsightsChart = ({ const items = payload.map(tooltipItemOf).filter(notEmpty); const activeEntity = findAlways(entities, e => e.id === id); - const ratio = activeEntity.change.ratio; const breakdowns = Object.keys(activeEntity.entities); if (breakdowns.length) { @@ -148,11 +147,13 @@ export const ProductInsightsChart = ({ title={title} subtitle={subtitle} topRight={ - + !!activeEntity.change.ratio && ( + + ) } actions={ @@ -173,11 +174,13 @@ export const ProductInsightsChart = ({ + !!activeEntity.change.ratio && ( + + ) } content={ id @@ -197,7 +200,9 @@ export const ProductInsightsChart = ({ return ( - + diff --git a/plugins/cost-insights/src/testUtils/mockData.ts b/plugins/cost-insights/src/testUtils/mockData.ts index 5149da278d..27bb07d0e1 100644 --- a/plugins/cost-insights/src/testUtils/mockData.ts +++ b/plugins/cost-insights/src/testUtils/mockData.ts @@ -294,7 +294,6 @@ export const MockBigQueryInsights: Entity = { id: 'dataset-c', aggregation: [0, 10_000], change: { - ratio: 10_000, amount: 10_000, }, entities: {}, @@ -415,7 +414,6 @@ export const MockCloudDataflowInsights: Entity = { id: 'pipeline-c', aggregation: [0, 10_000], change: { - ratio: 10_000, amount: 10_000, }, entities: {}, @@ -503,7 +501,6 @@ export const MockCloudStorageInsights: Entity = { id: 'Mock SKU C', aggregation: [2_000, 0], change: { - ratio: -1, amount: -2000, }, entities: {}, @@ -515,7 +512,6 @@ export const MockCloudStorageInsights: Entity = { id: 'bucket-c', aggregation: [0, 0], change: { - ratio: 0, amount: 0, }, entities: {}, @@ -655,7 +651,6 @@ export const MockComputeEngineInsights: Entity = { id: 'service-c', aggregation: [0, 10_000], change: { - ratio: 10_000, amount: 10_000, }, entities: {}, diff --git a/plugins/cost-insights/src/testUtils/testUtils.ts b/plugins/cost-insights/src/testUtils/testUtils.ts index 37913f635e..5077e05b75 100644 --- a/plugins/cost-insights/src/testUtils/testUtils.ts +++ b/plugins/cost-insights/src/testUtils/testUtils.ts @@ -93,10 +93,16 @@ export function changeOf(aggregation: DateAggregation[]): ChangeStatistic { const lastAmount = aggregation.length ? aggregation[aggregation.length - 1].amount : 0; - const ratio = - firstAmount !== 0 ? (lastAmount - firstAmount) / firstAmount : 0; + + // if either the first or last amounts are zero, the rate of increase/decrease is infinite + if (!firstAmount || !lastAmount) { + return { + amount: lastAmount - firstAmount, + }; + } + return { - ratio: ratio, + ratio: (lastAmount - firstAmount) / firstAmount, amount: lastAmount - firstAmount, }; } diff --git a/plugins/cost-insights/src/types/ChangeStatistic.ts b/plugins/cost-insights/src/types/ChangeStatistic.ts index a47640411a..70cd9fb9a2 100644 --- a/plugins/cost-insights/src/types/ChangeStatistic.ts +++ b/plugins/cost-insights/src/types/ChangeStatistic.ts @@ -16,7 +16,9 @@ export interface ChangeStatistic { // The ratio of change from one duration to another, expressed as: (newSum - oldSum) / oldSum - ratio: number; + // If a ratio cannot be calculated - such as when a new or old sum is zero, + // the ratio can be omitted and where applicable, ∞ or -∞ will display based on amount. + ratio?: number; // The actual USD change between time periods (can be negative if costs decreased) amount: number; } diff --git a/plugins/cost-insights/src/utils/assert.ts b/plugins/cost-insights/src/utils/assert.ts index 05ce65197b..5f9f0d01e1 100644 --- a/plugins/cost-insights/src/utils/assert.ts +++ b/plugins/cost-insights/src/utils/assert.ts @@ -20,7 +20,7 @@ export function notEmpty( return !isNull(value) && !isUndefined(value); } -export function isUndefined(value: any): boolean { +export function isUndefined(value: any): value is undefined { return value === undefined; } diff --git a/plugins/cost-insights/src/utils/change.test.ts b/plugins/cost-insights/src/utils/change.test.ts index 7208e9bc4f..191ae589d6 100644 --- a/plugins/cost-insights/src/utils/change.test.ts +++ b/plugins/cost-insights/src/utils/change.test.ts @@ -32,23 +32,23 @@ const GrowthMap = { describe.each` ratio | amount | expected - ${0.0} | ${undefined} | ${GrowthType.Negligible} + ${undefined} | ${0} | ${GrowthType.Negligible} + ${0.0} | ${0} | ${GrowthType.Negligible} ${0.0} | ${EngineerThreshold} | ${GrowthType.Negligible} ${ChangeThreshold.lower} | ${0} | ${GrowthType.Negligible} - ${ChangeThreshold.lower + 0.01} | ${undefined} | ${GrowthType.Negligible} + ${ChangeThreshold.lower + 0.01} | ${0} | ${GrowthType.Negligible} ${ChangeThreshold.lower + 0.01} | ${EngineerThreshold} | ${GrowthType.Negligible} ${ChangeThreshold.lower + 0.01} | ${EngineerThreshold + 0.1} | ${GrowthType.Negligible} ${ChangeThreshold.lower - 0.01} | ${EngineerThreshold - 0.1} | ${GrowthType.Negligible} - ${ChangeThreshold.upper - 0.01} | ${undefined} | ${GrowthType.Negligible} + ${ChangeThreshold.lower - 0.01} | ${0} | ${GrowthType.Negligible} + ${ChangeThreshold.upper} | ${0} | ${GrowthType.Negligible} + ${ChangeThreshold.upper - 0.01} | ${0} | ${GrowthType.Negligible} ${ChangeThreshold.upper + 0.01} | ${EngineerThreshold - 0.1} | ${GrowthType.Negligible} - ${ChangeThreshold.lower} | ${undefined} | ${GrowthType.Savings} + ${ChangeThreshold.upper + 0.01} | ${0} | ${GrowthType.Negligible} ${ChangeThreshold.lower} | ${EngineerThreshold} | ${GrowthType.Savings} - ${ChangeThreshold.lower - 0.01} | ${undefined} | ${GrowthType.Savings} ${ChangeThreshold.lower - 0.01} | ${EngineerThreshold} | ${GrowthType.Savings} ${ChangeThreshold.lower - 0.01} | ${EngineerThreshold + 0.1} | ${GrowthType.Savings} - ${ChangeThreshold.upper} | ${undefined} | ${GrowthType.Excess} ${ChangeThreshold.upper} | ${EngineerThreshold} | ${GrowthType.Excess} - ${ChangeThreshold.upper + 0.01} | ${undefined} | ${GrowthType.Excess} ${ChangeThreshold.upper + 0.01} | ${EngineerThreshold} | ${GrowthType.Excess} ${ChangeThreshold.upper + 0.01} | ${EngineerThreshold + 0.1} | ${GrowthType.Excess} `( @@ -63,7 +63,7 @@ describe.each` expected: GrowthType; }) => { it(`should display ${GrowthMap[expected]}`, () => { - expect(growthOf(ratio, amount)).toBe(expected); + expect(growthOf({ ratio, amount })).toBe(expected); }); }, ); diff --git a/plugins/cost-insights/src/utils/change.ts b/plugins/cost-insights/src/utils/change.ts index 950fbfeb82..479818bdae 100644 --- a/plugins/cost-insights/src/utils/change.ts +++ b/plugins/cost-insights/src/utils/change.ts @@ -27,21 +27,26 @@ import { import dayjs, { OpUnitType } from 'dayjs'; import durationPlugin from 'dayjs/plugin/duration'; import { inclusiveStartDateOf } from './duration'; +import { notEmpty } from './assert'; dayjs.extend(durationPlugin); // Used for displaying status colors -export function growthOf(ratio: number, amount?: number) { - if (typeof amount === 'number') { - if (amount >= EngineerThreshold && ratio >= ChangeThreshold.upper) { +export function growthOf(change: ChangeStatistic): GrowthType { + const exceedsEngineerThreshold = Math.abs(change.amount) >= EngineerThreshold; + + if (notEmpty(change.ratio)) { + if (exceedsEngineerThreshold && change.ratio >= ChangeThreshold.upper) { return GrowthType.Excess; } - if (amount >= EngineerThreshold && ratio <= ChangeThreshold.lower) { + + if (exceedsEngineerThreshold && change.ratio <= ChangeThreshold.lower) { return GrowthType.Savings; } } else { - if (ratio >= ChangeThreshold.upper) return GrowthType.Excess; - if (ratio <= ChangeThreshold.lower) return GrowthType.Savings; + if (exceedsEngineerThreshold && change.amount > 0) return GrowthType.Excess; + if (exceedsEngineerThreshold && change.amount < 0) + return GrowthType.Savings; } return GrowthType.Negligible; @@ -54,15 +59,24 @@ export function getComparedChange( duration: Duration, lastCompleteBillingDate: string, // YYYY-MM-DD, ): ChangeStatistic { - const ratio = dailyCost.change!.ratio - metricData.change.ratio; + const dailyCostRatio = dailyCost.change?.ratio; + const metricDataRatio = metricData.change?.ratio; const previousPeriodTotal = getPreviousPeriodTotalCost( dailyCost.aggregation, duration, lastCompleteBillingDate, ); + + // if either ratio cannot be calculated, no compared ratio can be calculated + if (!notEmpty(dailyCostRatio) || !notEmpty(metricDataRatio)) { + return { + amount: previousPeriodTotal, + }; + } + return { - ratio: ratio, - amount: previousPeriodTotal * ratio, + ratio: dailyCostRatio - metricDataRatio, + amount: previousPeriodTotal * (dailyCostRatio - metricDataRatio), }; } @@ -78,7 +92,6 @@ export function getPreviousPeriodTotalCost( ? [dayjsDuration.days(), 'day'] : [dayjsDuration.months(), 'month']; const nextPeriodStart = dayjs(startDate).add(amount, type); - // Add up costs that incurred before the start of the next period. return aggregation.reduce((acc, costByDate) => { return dayjs(costByDate.date).isBefore(nextPeriodStart) @@ -86,3 +99,11 @@ export function getPreviousPeriodTotalCost( : acc; }, 0); } + +export function choose( + [savings, excess]: [T, T], + change: ChangeStatistic, +): T { + const isSavings = (change.ratio ?? change.amount) <= 0; + return isSavings ? savings : excess; +} diff --git a/plugins/cost-insights/src/utils/formatters.ts b/plugins/cost-insights/src/utils/formatters.ts index 8f2b94f997..75306d7392 100644 --- a/plugins/cost-insights/src/utils/formatters.ts +++ b/plugins/cost-insights/src/utils/formatters.ts @@ -16,8 +16,9 @@ import moment from 'moment'; import pluralize from 'pluralize'; -import { Duration } from '../types'; +import { ChangeStatistic, Duration } from '../types'; import { inclusiveEndDateOf, inclusiveStartDateOf } from '../utils/duration'; +import { notEmpty } from './assert'; export type Period = { periodStart: string; @@ -78,6 +79,13 @@ export function formatCurrency(amount: number, currency?: string): string { return currency ? `${numString} ${pluralize(currency, n)}` : numString; } +export function formatChange(change: ChangeStatistic): string { + if (notEmpty(change.ratio)) { + return formatPercent(Math.abs(change.ratio)); + } + return change.amount >= 0 ? '∞' : '-∞'; +} + export function formatPercent(n: number): string { // Number.toFixed shows scientific notation for extreme numbers if (isNaN(n) || Math.abs(n) < 0.01) { From 6f1b82b14175f85a76a7b91db9800ec8f6425d81 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Thu, 22 Apr 2021 15:03:17 -0400 Subject: [PATCH 02/18] changeset Signed-off-by: Ryan Vazquez --- .changeset/cost-insights-five-baboons-attack.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cost-insights-five-baboons-attack.md diff --git a/.changeset/cost-insights-five-baboons-attack.md b/.changeset/cost-insights-five-baboons-attack.md new file mode 100644 index 0000000000..f2c070acb4 --- /dev/null +++ b/.changeset/cost-insights-five-baboons-attack.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': minor +--- + +make change ratio optional From c2abcf91acc397b9837eff29f0dffbcbdfb110da Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Thu, 22 Apr 2021 15:41:58 -0400 Subject: [PATCH 03/18] lint Signed-off-by: Ryan Vazquez --- .../src/components/CostOverviewCard/CostOverviewLegend.tsx | 2 -- .../src/components/ProductInsightsCard/ProductEntityTable.tsx | 1 - 2 files changed, 3 deletions(-) diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewLegend.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewLegend.tsx index 0033ba4f14..ad8a065a6e 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewLegend.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewLegend.tsx @@ -30,8 +30,6 @@ import { mapFiltersToProps } from './selector'; import { formatChange } from '../../utils/formatters'; import { CostGrowth } from '../CostGrowth'; -import { notEmpty } from '../../utils/assert'; - type CostOverviewLegendProps = { metric: Maybe; metricData: Maybe; diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx index a8211fbbed..f9bb44058b 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx @@ -22,7 +22,6 @@ import { costFormatter, formatChange } from '../../utils/formatters'; import { useEntityDialogStyles as useStyles } from '../../utils/styles'; import { CostGrowthIndicator } from '../CostGrowth'; import { BarChartOptions, ChangeStatistic, Entity } from '../../types'; -import { isUndefined } from '../../utils/assert'; export type ProductEntityTableOptions = Partial< Pick From 6c38f5e92031b5574e20d8da28ffeedd89608177 Mon Sep 17 00:00:00 2001 From: Brian Fox Date: Tue, 27 Apr 2021 10:55:57 +0200 Subject: [PATCH 04/18] tests(proxy-backend): align tests with default backend configuration The pathPrefix is not suffixed with `/` by default as noted at https://github.com/backstage/backstage/blob/df897a6126d1641d111b015c527cc8d40c47bd06/packages/backend-common/src/discovery/types.ts#L47 and seen in the implementation at https://github.com/backstage/backstage/blob/df897a6126d1641d111b015c527cc8d40c47bd06/packages/backend-common/src/discovery/SingleHostDiscovery.ts#L81. Currently the tests work in the opposite way - the pathPrefix is suffixed by / whereas the routes are **not** prefixed by /. This commit brings the tests in line with the default configuration prior to any further work. Signed-off-by: Brian Fox --- .../proxy-backend/src/service/router.test.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/proxy-backend/src/service/router.test.ts b/plugins/proxy-backend/src/service/router.test.ts index 6c47043602..3a0a25f164 100644 --- a/plugins/proxy-backend/src/service/router.test.ts +++ b/plugins/proxy-backend/src/service/router.test.ts @@ -60,7 +60,7 @@ describe('buildMiddleware', () => { }); it('accepts strings', async () => { - buildMiddleware('/api/', logger, 'test', 'http://mocked'); + buildMiddleware('/proxy', logger, '/test', 'http://mocked'); expect(createProxyMiddleware).toHaveBeenCalledTimes(1); @@ -74,13 +74,13 @@ describe('buildMiddleware', () => { expect(filter('', { method: 'PATCH', headers: {} })).toBe(true); expect(filter('', { method: 'DELETE', headers: {} })).toBe(true); - expect(fullConfig.pathRewrite).toEqual({ '^/api/test/': '/' }); + expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/': '/' }); expect(fullConfig.changeOrigin).toBe(true); expect(fullConfig.logProvider!(logger)).toBe(logger); }); it('limits allowedMethods', async () => { - buildMiddleware('/api/', logger, 'test', { + buildMiddleware('/proxy', logger, '/test', { target: 'http://mocked', allowedMethods: ['GET', 'DELETE'], }); @@ -97,13 +97,13 @@ describe('buildMiddleware', () => { expect(filter('', { method: 'PATCH', headers: {} })).toBe(false); expect(filter('', { method: 'DELETE', headers: {} })).toBe(true); - expect(fullConfig.pathRewrite).toEqual({ '^/api/test/': '/' }); + expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/': '/' }); expect(fullConfig.changeOrigin).toBe(true); expect(fullConfig.logProvider!(logger)).toBe(logger); }); it('permits default headers', async () => { - buildMiddleware('/api/', logger, 'test', { + buildMiddleware('/proxy', logger, '/test', { target: 'http://mocked', }); @@ -143,7 +143,7 @@ describe('buildMiddleware', () => { }); it('permits default and configured headers', async () => { - buildMiddleware('/api/', logger, 'test', { + buildMiddleware('/proxy', logger, '/test', { target: 'http://mocked', headers: { Authorization: 'my-token', @@ -176,7 +176,7 @@ describe('buildMiddleware', () => { }); it('permits configured headers', async () => { - buildMiddleware('/api/', logger, 'test', { + buildMiddleware('/proxy', logger, '/test', { target: 'http://mocked', allowedHeaders: ['authorization', 'cookie'], }); @@ -208,7 +208,7 @@ describe('buildMiddleware', () => { }); it('responds default headers', async () => { - buildMiddleware('/api/', logger, 'test', { + buildMiddleware('/proxy', logger, '/test', { target: 'http://mocked', }); @@ -251,7 +251,7 @@ describe('buildMiddleware', () => { }); it('responds configured headers', async () => { - buildMiddleware('/api/', logger, 'test', { + buildMiddleware('/proxy', logger, '/test', { target: 'http://mocked', allowedHeaders: ['set-cookie'], }); @@ -282,10 +282,10 @@ describe('buildMiddleware', () => { it('rejects malformed target URLs', async () => { expect(() => - buildMiddleware('/api/', logger, 'test', 'backstage.io'), + buildMiddleware('/proxy', logger, '/test', 'backstage.io'), ).toThrowError(/Proxy target is not a valid URL/); expect(() => - buildMiddleware('/api/', logger, 'test', { target: 'backstage.io' }), + buildMiddleware('/proxy', logger, '/test', { target: 'backstage.io' }), ).toThrowError(/Proxy target is not a valid URL/); }); }); From cdb3426e56f49dd5dffba3c35eea383a416b1bf1 Mon Sep 17 00:00:00 2001 From: Brian Fox Date: Tue, 27 Apr 2021 12:38:31 +0200 Subject: [PATCH 05/18] fix(proxy-backend): prefix routes with `/` if not present in config This commit ensures that the default `pathRewrite` configuration for a proxy element is more resilient to the combination of whether: - `route` is prefixed with `/` - `pathPrefix` on the plugin is suffixed with `/` (which at present it will never be - see comments). Signed-off-by: Brian Fox --- .changeset/thick-cobras-switch.md | 5 +++ docs/plugins/proxying.md | 11 ++--- .../proxy-backend/src/service/router.test.ts | 42 ++++++++++++++++++- plugins/proxy-backend/src/service/router.ts | 16 ++++++- 4 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 .changeset/thick-cobras-switch.md diff --git a/.changeset/thick-cobras-switch.md b/.changeset/thick-cobras-switch.md new file mode 100644 index 0000000000..002c724421 --- /dev/null +++ b/.changeset/thick-cobras-switch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-proxy-backend': patch +--- + +Prefix proxy routes with `/` if not present in configuration diff --git a/docs/plugins/proxying.md b/docs/plugins/proxying.md index 50c56af7a6..3e20e37cad 100644 --- a/docs/plugins/proxying.md +++ b/docs/plugins/proxying.md @@ -36,7 +36,7 @@ Example: ```yaml # in app-config.yaml proxy: - '/simple-example': http://simple.example.com:8080 + simple-example: http://simple.example.com:8080 '/larger-example/v1': target: http://larger.example.com:8080/svc.v1 headers: @@ -46,10 +46,11 @@ proxy: ``` Each key under the proxy configuration entry is a route to match, below the -prefix that the proxy plugin is mounted on. It must start with a slash. For -example, if the backend mounts the proxy plugin as `/proxy`, the above -configuration will lead to the proxy acting on backend requests to -`/api/proxy/simple-example/...` and `/api/proxy/larger-example/v1/...`. +prefix that the proxy plugin is mounted on. If it does not start with a slash, +one will be prefixed automatically. For example, if the backend mounts the proxy +plugin as `/proxy`, the above configuration will lead to the proxy acting on +backend requests to `/api/proxy/simple-example/...` and +`/api/proxy/larger-example/v1/...`. The value inside each route is either a simple URL string, or an object on the format accepted by diff --git a/plugins/proxy-backend/src/service/router.test.ts b/plugins/proxy-backend/src/service/router.test.ts index 3a0a25f164..571de96ac4 100644 --- a/plugins/proxy-backend/src/service/router.test.ts +++ b/plugins/proxy-backend/src/service/router.test.ts @@ -59,7 +59,7 @@ describe('buildMiddleware', () => { mockCreateProxyMiddleware.mockClear(); }); - it('accepts strings', async () => { + it('accepts strings prefixed by /', async () => { buildMiddleware('/proxy', logger, '/test', 'http://mocked'); expect(createProxyMiddleware).toHaveBeenCalledTimes(1); @@ -79,6 +79,46 @@ describe('buildMiddleware', () => { expect(fullConfig.logProvider!(logger)).toBe(logger); }); + it('accepts routes not prefixed with / when path is not suffixed with /', async () => { + buildMiddleware('/proxy', logger, 'test', 'http://mocked'); + + expect(createProxyMiddleware).toHaveBeenCalledTimes(1); + + const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [ + (pathname: string, req: Partial) => boolean, + ProxyMiddlewareConfig, + ]; + expect(filter('', { method: 'GET', headers: {} })).toBe(true); + expect(filter('', { method: 'POST', headers: {} })).toBe(true); + expect(filter('', { method: 'PUT', headers: {} })).toBe(true); + expect(filter('', { method: 'PATCH', headers: {} })).toBe(true); + expect(filter('', { method: 'DELETE', headers: {} })).toBe(true); + + expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/': '/' }); + expect(fullConfig.changeOrigin).toBe(true); + expect(fullConfig.logProvider!(logger)).toBe(logger); + }); + + it('accepts routes prefixed with / when path is suffixed with /', async () => { + buildMiddleware('/proxy/', logger, '/test', 'http://mocked'); + + expect(createProxyMiddleware).toHaveBeenCalledTimes(1); + + const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [ + (pathname: string, req: Partial) => boolean, + ProxyMiddlewareConfig, + ]; + expect(filter('', { method: 'GET', headers: {} })).toBe(true); + expect(filter('', { method: 'POST', headers: {} })).toBe(true); + expect(filter('', { method: 'PUT', headers: {} })).toBe(true); + expect(filter('', { method: 'PATCH', headers: {} })).toBe(true); + expect(filter('', { method: 'DELETE', headers: {} })).toBe(true); + + expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/': '/' }); + expect(fullConfig.changeOrigin).toBe(true); + expect(fullConfig.logProvider!(logger)).toBe(logger); + }); + it('limits allowedMethods', async () => { buildMiddleware('/proxy', logger, '/test', { target: 'http://mocked', diff --git a/plugins/proxy-backend/src/service/router.ts b/plugins/proxy-backend/src/service/router.ts index 5c306de966..71ed82c23b 100644 --- a/plugins/proxy-backend/src/service/router.ts +++ b/plugins/proxy-backend/src/service/router.ts @@ -77,10 +77,24 @@ export function buildMiddleware( `Proxy target is not a valid URL: ${fullConfig.target ?? ''}`, ); } + // Default is to do a path rewrite that strips out the proxy's path prefix // and the rest of the route. if (fullConfig.pathRewrite === undefined) { - const routeWithSlash = route.endsWith('/') ? route : `${route}/`; + let routeWithSlash = route.endsWith('/') ? route : `${route}/`; + + if (!pathPrefix.endsWith('/') && !routeWithSlash.startsWith('/')) { + // Need to insert a / between pathPrefix and routeWithSlash + routeWithSlash = `/${routeWithSlash}`; + } else if (pathPrefix.endsWith('/') && routeWithSlash.startsWith('/')) { + // Never expect this to happen at this point in time as + // pathPrefix is set using `getExternalBaseUrl` which "Returns the + // external HTTP base backend URL for a given plugin, + // **without a trailing slash.**". But in case this changes in future, we + // need to drop a / on either pathPrefix or routeWithSlash + routeWithSlash = routeWithSlash.substring(1); + } + fullConfig.pathRewrite = { [`^${pathPrefix}${routeWithSlash}`]: '/', }; From c04403f6195cfebd1e0b99e2525965d149a2a68c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Apr 2021 04:26:55 +0000 Subject: [PATCH 06/18] chore(deps): bump json-schema from 0.2.5 to 0.3.0 Bumps [json-schema](https://github.com/kriszyp/json-schema) from 0.2.5 to 0.3.0. - [Release notes](https://github.com/kriszyp/json-schema/releases) - [Commits](https://github.com/kriszyp/json-schema/compare/v0.2.5...v0.3.0) Signed-off-by: dependabot[bot] --- packages/catalog-model/package.json | 2 +- packages/cli/package.json | 2 +- packages/config-loader/package.json | 2 +- plugins/scaffolder/package.json | 2 +- yarn.lock | 17 +++++------------ 5 files changed, 9 insertions(+), 16 deletions(-) diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 64550b35d1..941877aec5 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -33,7 +33,7 @@ "@types/json-schema": "^7.0.5", "@types/yup": "^0.29.8", "ajv": "^7.0.3", - "json-schema": "^0.2.5", + "json-schema": "^0.3.0", "lodash": "^4.17.15", "uuid": "^8.0.0", "yup": "^0.29.3" diff --git a/packages/cli/package.json b/packages/cli/package.json index cd61193efe..1110efefe6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -82,7 +82,7 @@ "inquirer": "^7.0.4", "jest": "^26.0.1", "jest-css-modules": "^2.1.0", - "json-schema": "^0.2.5", + "json-schema": "^0.3.0", "lodash": "^4.17.19", "mini-css-extract-plugin": "^0.9.0", "ora": "^5.3.0", diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 0a0a76c2d6..55d86314af 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -35,7 +35,7 @@ "@types/json-schema": "^7.0.6", "ajv": "^7.0.3", "fs-extra": "^9.0.0", - "json-schema": "^0.2.5", + "json-schema": "^0.3.0", "json-schema-merge-allof": "^0.7.0", "typescript-json-schema": "^0.49.0", "yaml": "^1.9.2", diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 2b4feecb7b..8af5339a0f 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -44,7 +44,7 @@ "@rjsf/core": "^2.4.0", "@rjsf/material-ui": "^2.4.0", "classnames": "^2.2.6", - "json-schema": "^0.2.5", + "json-schema": "^0.3.0", "git-url-parse": "^11.4.4", "humanize-duration": "^3.25.1", "immer": "^9.0.1", diff --git a/yarn.lock b/yarn.lock index b21e2a9bf5..6076e21713 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16965,10 +16965,10 @@ json-schema@0.2.3: resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= -json-schema@^0.2.5: - version "0.2.5" - resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.5.tgz#97997f50972dd0500214e208c407efa4b5d7063b" - integrity sha512-gWJOWYFrhQ8j7pVm0EM8Slr+EPVq1Phf6lvzvD/WCeqkrx/f2xBI0xOsRRS9xCn3I4vKtP519dvs3TP09r24wQ== +json-schema@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.3.0.tgz#90a9c5054bd065422c00241851ce8d59475b701b" + integrity sha512-TYfxx36xfl52Rf1LU9HyWSLGPdYLL+SQ8/E/0yVyKG8wCCDaSrhPap0vEdlsZWRaS6tnKKLPGiEJGiREVC8kxQ== json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" @@ -23538,7 +23538,7 @@ semver@7.0.0: resolved "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4: +semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@~7.3.0: version "7.3.5" resolved "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== @@ -23555,13 +23555,6 @@ semver@~5.3.0: resolved "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= -semver@~7.3.0: - version "7.3.5" - resolved "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== - dependencies: - lru-cache "^6.0.0" - send@0.17.1: version "0.17.1" resolved "https://registry.npmjs.org/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" From d1b1306d98a51f4a6e9814fb18a4abdb7179c2e8 Mon Sep 17 00:00:00 2001 From: Will Date: Tue, 27 Apr 2021 15:36:01 +0100 Subject: [PATCH 07/18] catalog-client: allow `filter` param to be specified multiple times This is possible on the backend, see: - https://github.com/backstage/backstage/blob/f9d077f5f6c3bf1e20255359798dd1febbeb3e27/plugins/catalog-backend/src/database/types.ts#L129-L131 - https://github.com/backstage/backstage/blob/f9d077f5f6c3bf1e20255359798dd1febbeb3e27/plugins/catalog-backend/src/service/request/parseEntityFilterParams.ts#L25-L42 Signed-off-by: Will Sewell --- .changeset/twenty-peas-deny.md | 5 +++ .../catalog-client/src/CatalogClient.test.ts | 32 +++++++++++++++++++ packages/catalog-client/src/CatalogClient.ts | 25 ++++++++++----- packages/catalog-client/src/types.ts | 5 ++- 4 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 .changeset/twenty-peas-deny.md diff --git a/.changeset/twenty-peas-deny.md b/.changeset/twenty-peas-deny.md new file mode 100644 index 0000000000..61c9cb3587 --- /dev/null +++ b/.changeset/twenty-peas-deny.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': minor +--- + +Allow `filter` param to be specified multiple times diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index 19da45e4ec..fefe670a55 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -79,6 +79,37 @@ describe('CatalogClient', () => { it('builds entity search filters properly', async () => { expect.assertions(2); + server.use( + rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => { + expect(req.url.search).toBe( + '?filter=a=1,b=2,b=3,%C3%B6=%3D&filter=a=2', + ); + return res(ctx.json([])); + }), + ); + + const response = await client.getEntities( + { + filter: [ + { + a: '1', + b: ['2', '3'], + ö: '=', + }, + { + a: '2', + }, + ], + }, + { token }, + ); + + expect(response.items).toEqual([]); + }); + + it('builds entity legacy search filters properly', async () => { + expect.assertions(2); + server.use( rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => { expect(req.url.search).toBe('?filter=a=1,b=2,b=3,%C3%B6=%3D'); @@ -88,6 +119,7 @@ describe('CatalogClient', () => { const response = await client.getEntities( { + // The legacy value of filter is not an array filter: { a: '1', b: ['2', '3'], diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 3de25e9808..a246b3abab 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -56,18 +56,27 @@ export class CatalogClient implements CatalogApi { request?: CatalogEntitiesRequest, options?: CatalogRequestOptions, ): Promise> { - const { filter = {}, fields = [] } = request ?? {}; + const { filter = [], fields = [] } = request ?? {}; + const filterItems = [filter].flat(); const params: string[] = []; - const filterParts: string[] = []; - for (const [key, value] of Object.entries(filter)) { - for (const v of [value].flat()) { - filterParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`); + // filter param can occur multiple times, for example + // /api/catalog/entities?filter=metadata.name=wayback-search,kind=component&filter=metadata.name=www-artist,kind=component' + // the "outer array" defined by `filter` occurrences corresponds to "anyOf" filters + // the "inner array" defined within a `filter` param corresponds to "allOf" filters + for (const filterItem of filterItems) { + const filterParts: string[] = []; + for (const [key, value] of Object.entries(filterItem)) { + for (const v of [value].flat()) { + filterParts.push( + `${encodeURIComponent(key)}=${encodeURIComponent(v)}`, + ); + } } - } - if (filterParts.length) { - params.push(`filter=${filterParts.join(',')}`); + if (filterParts.length) { + params.push(`filter=${filterParts.join(',')}`); + } } if (fields.length) { diff --git a/packages/catalog-client/src/types.ts b/packages/catalog-client/src/types.ts index 0d25bf7483..9035657ddc 100644 --- a/packages/catalog-client/src/types.ts +++ b/packages/catalog-client/src/types.ts @@ -17,7 +17,10 @@ import { Entity, EntityName, Location } from '@backstage/catalog-model'; export type CatalogEntitiesRequest = { - filter?: Record | undefined; + filter?: + | Record[] + | Record // Legacy type preserved for backwards compatibility + | undefined; fields?: string[] | undefined; }; From 17644ca68eb67dea67b1691f34181c61b811326a Mon Sep 17 00:00:00 2001 From: Will Date: Wed, 28 Apr 2021 14:23:10 +0100 Subject: [PATCH 08/18] Don't treat singuler `filter` type as legacy Instead it's a convenience type for when only a single instance of a param is required. Signed-off-by: Will Sewell --- packages/catalog-client/src/CatalogClient.test.ts | 5 ++--- packages/catalog-client/src/types.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index fefe670a55..359e3a2c60 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -76,7 +76,7 @@ describe('CatalogClient', () => { expect(response).toEqual(defaultResponse); }); - it('builds entity search filters properly', async () => { + it('builds multiple entity search filters properly', async () => { expect.assertions(2); server.use( @@ -107,7 +107,7 @@ describe('CatalogClient', () => { expect(response.items).toEqual([]); }); - it('builds entity legacy search filters properly', async () => { + it('builds single entity search filter properly', async () => { expect.assertions(2); server.use( @@ -119,7 +119,6 @@ describe('CatalogClient', () => { const response = await client.getEntities( { - // The legacy value of filter is not an array filter: { a: '1', b: ['2', '3'], diff --git a/packages/catalog-client/src/types.ts b/packages/catalog-client/src/types.ts index 9035657ddc..ef907eafa9 100644 --- a/packages/catalog-client/src/types.ts +++ b/packages/catalog-client/src/types.ts @@ -19,7 +19,7 @@ import { Entity, EntityName, Location } from '@backstage/catalog-model'; export type CatalogEntitiesRequest = { filter?: | Record[] - | Record // Legacy type preserved for backwards compatibility + | Record | undefined; fields?: string[] | undefined; }; From bd877d4518ec75be2ce9d63ad6cd26da93d1808c Mon Sep 17 00:00:00 2001 From: Will Date: Wed, 28 Apr 2021 14:25:05 +0100 Subject: [PATCH 09/18] Make "patch"-level change instead of "minor" Signed-off-by: Will Sewell --- .changeset/twenty-peas-deny.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/twenty-peas-deny.md b/.changeset/twenty-peas-deny.md index 61c9cb3587..df335ea95a 100644 --- a/.changeset/twenty-peas-deny.md +++ b/.changeset/twenty-peas-deny.md @@ -1,5 +1,5 @@ --- -'@backstage/catalog-client': minor +'@backstage/catalog-client': patch --- Allow `filter` param to be specified multiple times From d8b81fd28bf3cacf96cf62739e092f01568234fb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 28 Apr 2021 16:11:29 +0200 Subject: [PATCH 10/18] changesets: add changeset for json-schema bump Signed-off-by: Patrik Oldsberg --- .changeset/tough-walls-wash.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/tough-walls-wash.md diff --git a/.changeset/tough-walls-wash.md b/.changeset/tough-walls-wash.md new file mode 100644 index 0000000000..a45adcfbf6 --- /dev/null +++ b/.changeset/tough-walls-wash.md @@ -0,0 +1,9 @@ +--- +'@backstage/catalog-model': patch +'@backstage/cli': patch +'@backstage/config-loader': patch +'@backstage/config': patch +'@backstage/plugin-scaffolder': patch +--- + +Bump `json-schema` dependency from `0.2.5` to `0.3.0`. From e9e56b01ac3d115c81fc511c8f82afa36ce9a9c8 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Thu, 29 Apr 2021 09:43:10 +0200 Subject: [PATCH 11/18] Add possibility to use path style discovery on S3 tech docs. Enables the possibility to support S3-like buckets that rely on path style API like Localstack. Signed-off-by: Jussi Hallila --- .changeset/tricky-yaks-melt.md | 6 ++++++ .github/styles/vocab.txt | 2 ++ docs/features/techdocs/configuration.md | 5 +++++ packages/techdocs-common/src/stages/publish/awsS3.ts | 7 +++++++ plugins/techdocs-backend/config.d.ts | 7 +++++++ 5 files changed, 27 insertions(+) create mode 100644 .changeset/tricky-yaks-melt.md diff --git a/.changeset/tricky-yaks-melt.md b/.changeset/tricky-yaks-melt.md new file mode 100644 index 0000000000..3679e6d1da --- /dev/null +++ b/.changeset/tricky-yaks-melt.md @@ -0,0 +1,6 @@ +--- +'@backstage/techdocs-common': patch +--- + +Adding optional config to enable S3-like API for tech-docs using s3ForcePathStyle option. +This allows providers like LocalStack, Minio and Wasabi (+possibly others) to be used to host tech docs. diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index a12f638a74..694226b8cc 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -41,8 +41,10 @@ Kaewkasi Knex Leasot Lerna +LocalStack Luxon Minikube +Minio Mkdocs Monorepo Namespaces diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index c7876f3874..aa38dd42b1 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -78,6 +78,11 @@ techdocs: # https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property endpoint: ${AWS_ENDPOINT} + # (Optional) Whether to use path style URLs when communicating with S3. + # Defaults to false. + # This allows providers like LocalStack, Minio and Wasabi (and possibly others) to be used to host tech docs. + s3ForcePathStyle: false + # Required when techdocs.publisher.type is set to 'azureBlobStorage'. Skip otherwise. azureBlobStorage: diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index 9c6684fdca..d628c860fd 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -80,10 +80,17 @@ export class AwsS3Publish implements PublisherBase { 'techdocs.publisher.awsS3.endpoint', ); + // AWS forcePathStyle is an optional config. If missing, it defaults to false. Needs to be enabled for cases + // where endpoint url points to locally hosted S3 compatible storage like Localstack + const s3ForcePathStyle = config.getOptionalBoolean( + 'techdocs.publisher.awsS3.s3ForcePathStyle', + ); + const storageClient = new aws.S3({ credentials, ...(region && { region }), ...(endpoint && { endpoint }), + ...(s3ForcePathStyle && { s3ForcePathStyle }), }); return new AwsS3Publish(storageClient, bucketName, logger); diff --git a/plugins/techdocs-backend/config.d.ts b/plugins/techdocs-backend/config.d.ts index 83b79f2f6c..57dbb5108e 100644 --- a/plugins/techdocs-backend/config.d.ts +++ b/plugins/techdocs-backend/config.d.ts @@ -90,6 +90,13 @@ export interface Config { * @visibility secret */ endpoint?: string; + /** + * (Optional) Whether to use path style URLs when communicating with S3. + * Defaults to false. + * This allows providers like LocalStack, Minio and Wasabi (and possibly others) to be used to host tech docs. + * @visibility backend + */ + endpoint?: string; }; } | { From 86891fd2742b8b7624cb80619781aff0383b3385 Mon Sep 17 00:00:00 2001 From: Will Sewell Date: Thu, 29 Apr 2021 12:36:41 +0100 Subject: [PATCH 12/18] Rename 'param' -> 'parameter' in changeset Signed-off-by: Will Sewell --- .changeset/twenty-peas-deny.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/twenty-peas-deny.md b/.changeset/twenty-peas-deny.md index df335ea95a..d149a9337c 100644 --- a/.changeset/twenty-peas-deny.md +++ b/.changeset/twenty-peas-deny.md @@ -2,4 +2,4 @@ '@backstage/catalog-client': patch --- -Allow `filter` param to be specified multiple times +Allow `filter` parameter to be specified multiple times From b2e2ec75337768330b277365bfc689cbc77188b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 29 Apr 2021 15:43:53 +0200 Subject: [PATCH 13/18] Update tech radar README for composability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/spotty-pigs-bathe.md | 5 +++++ plugins/tech-radar/README.md | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 .changeset/spotty-pigs-bathe.md diff --git a/.changeset/spotty-pigs-bathe.md b/.changeset/spotty-pigs-bathe.md new file mode 100644 index 0000000000..0585f335dc --- /dev/null +++ b/.changeset/spotty-pigs-bathe.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-radar': patch +--- + +Update README for composability diff --git a/plugins/tech-radar/README.md b/plugins/tech-radar/README.md index 21f90ec432..7f97f88ed4 100644 --- a/plugins/tech-radar/README.md +++ b/plugins/tech-radar/README.md @@ -18,7 +18,7 @@ It serves and scales well for teams and companies of all sizes that want to have The Tech Radar can be used in two ways: -- **Simple (Recommended)** - This gives you an out-of-the-box Tech Radar experience. It lives on the `/tech-radar` URL of your Backstage installation, and you can set a variety of configuration directly in your `apis.ts`. +- **Simple (Recommended)** - This gives you an out-of-the-box Tech Radar experience. It lives on the `/tech-radar` URL of your Backstage installation. - **Advanced** - This gives you the React UI component directly. It enables you to insert the Radar on your own layout or page for a more customized feel. ### Install @@ -26,6 +26,7 @@ The Tech Radar can be used in two ways: For either simple or advanced installations, you'll need to add the dependency using Yarn: ```sh +cd packages/app yarn add @backstage/plugin-tech-radar ``` @@ -34,17 +35,16 @@ yarn add @backstage/plugin-tech-radar Modify your app routes to include the Router component exported from the tech radar, for example: ```tsx -import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar'; +// in packages/app/src/App.tsx +import { TechRadarPage } from '@backstage/plugin-tech-radar'; -// Inside App component - - {/* other routes ... */} - } - /> - {/* other routes ... */} -; +const routes = ( + + {/* ... */} + } + /> ``` If you'd like to configure it more, see the `TechRadarPageProps` and `TechRadarComponentProps` types for options: From 4e6168da41070c0f58d2f7bf95ed7a433a6e5f52 Mon Sep 17 00:00:00 2001 From: Will Sewell Date: Thu, 29 Apr 2021 16:16:24 +0100 Subject: [PATCH 14/18] Update public API docs Result of running `yarn build:api-reports`. Signed-off-by: Will Sewell --- packages/catalog-client/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index a17a6cdc10..4b489ff2e4 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -67,7 +67,7 @@ export class CatalogClient implements CatalogApi { // @public (undocumented) export type CatalogEntitiesRequest = { - filter?: Record | undefined; + filter?: Record[] | Record | undefined; fields?: string[] | undefined; }; From 7c8a0c1b09a2d8319a7d5daad63b80d6eb2bea3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Apr 2021 04:08:24 +0000 Subject: [PATCH 15/18] chore(deps-dev): bump @types/luxon from 1.26.0 to 1.26.5 Bumps [@types/luxon](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/luxon) from 1.26.0 to 1.26.5. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/luxon) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index e5bbc026c6..6d1cabbc22 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6295,9 +6295,9 @@ integrity sha512-09sXZZVsB3Ib41U0fC+O1O+4UOZT1bl/e+/QubPxpqDWHNEchvx/DEb1KJMOwq6K3MTNzZFoNSzVdR++o1DVnw== "@types/luxon@^1.25.0": - version "1.26.0" - resolved "https://registry.npmjs.org/@types/luxon/-/luxon-1.26.0.tgz#8e783986370ad3bb9f885d93eb1a91caeecaed36" - integrity sha512-zYmLYGczqBaOFaFjR1giG1QCbGMlXWOJcYgH9Mnk0MGcZHq1aer3ZwGXX8vd9NOfai6mAI/mVU6jD9hK0Wys7Q== + version "1.26.5" + resolved "https://registry.npmjs.org/@types/luxon/-/luxon-1.26.5.tgz#843fb705e16e4d2a90847a351b799ea9d879859e" + integrity sha512-XeQxxRMyJi1znfzHw4CGDLyup/raj84SnjjkI2fDootZPGlB0yqtvlvEIAmzHDa5wiEI5JJevZOWxpcofsaV+A== "@types/markdown-to-jsx@^6.11.0": version "6.11.2" From 799c1d69cf4e009e603c215cf788553e7c927508 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Apr 2021 04:11:42 +0000 Subject: [PATCH 16/18] chore(deps-dev): bump start-server-and-test from 1.12.0 to 1.12.1 Bumps [start-server-and-test](https://github.com/bahmutov/start-server-and-test) from 1.12.0 to 1.12.1. - [Release notes](https://github.com/bahmutov/start-server-and-test/releases) - [Commits](https://github.com/bahmutov/start-server-and-test/compare/v1.12.0...v1.12.1) Signed-off-by: dependabot[bot] --- yarn.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index e5bbc026c6..0fd3ad76c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24297,9 +24297,9 @@ stacktrace-js@^2.0.0, stacktrace-js@^2.0.2: stacktrace-gps "^3.0.4" start-server-and-test@^1.10.11: - version "1.12.0" - resolved "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-1.12.0.tgz#e836553c928a13026f79c740757d378b92bee8d6" - integrity sha512-y3M/PLUPkPBsgKoengMIMQeceT8uOnOc4bkdor/RSCK9Ih/j8z4WthSCrAboXLjgtJJWOporAiEQsnYox+THXg== + version "1.12.1" + resolved "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-1.12.1.tgz#bf84eb5c5a4c8a98b93ed36519035b3f76179f0e" + integrity sha512-qGQ2HQiF2yDIfyaHsXkHfoE5UOl4zJUbJ/gx2xOkfX7iPMXW9qHmoFyaMfIDJVLNkxCK7RxSrvWEI9hNVKQluw== dependencies: bluebird "3.7.2" check-more-types "2.24.0" @@ -24307,7 +24307,7 @@ start-server-and-test@^1.10.11: execa "3.4.0" lazy-ass "1.6.0" ps-tree "1.2.0" - wait-on "5.2.1" + wait-on "5.3.0" start-server-webpack-plugin@^2.2.5: version "2.2.5" @@ -26420,14 +26420,14 @@ w3c-xmlserializer@^2.0.0: dependencies: xml-name-validator "^3.0.0" -wait-on@5.2.1: - version "5.2.1" - resolved "https://registry.npmjs.org/wait-on/-/wait-on-5.2.1.tgz#05b66fcb4d7f5da01537f03e7cf96e8836422996" - integrity sha512-H2F986kNWMU9hKlI9l/ppO6tN8ZSJd35yBljMLa1/vjzWP++Qh6aXyt77/u7ySJFZQqBtQxnvm/xgG48AObXcw== +wait-on@5.3.0: + version "5.3.0" + resolved "https://registry.npmjs.org/wait-on/-/wait-on-5.3.0.tgz#584e17d4b3fe7b46ac2b9f8e5e102c005c2776c7" + integrity sha512-DwrHrnTK+/0QFaB9a8Ol5Lna3k7WvUR4jzSKmz0YaPBpuN2sACyiPVKVfj6ejnjcajAcvn3wlbTyMIn9AZouOg== dependencies: axios "^0.21.1" joi "^17.3.0" - lodash "^4.17.20" + lodash "^4.17.21" minimist "^1.2.5" rxjs "^6.6.3" From d8d22dd9332ba4cbfd413e199526580a398c2418 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Apr 2021 04:14:12 +0000 Subject: [PATCH 17/18] chore(deps-dev): bump @types/passport-google-oauth20 from 2.0.3 to 2.0.7 Bumps [@types/passport-google-oauth20](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/passport-google-oauth20) from 2.0.3 to 2.0.7. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/passport-google-oauth20) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index e5bbc026c6..3723040450 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6453,9 +6453,9 @@ "@types/passport-oauth2" "*" "@types/passport-google-oauth20@^2.0.3": - version "2.0.3" - resolved "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.3.tgz#f554ff6d39f395acff3f1d762e54462194dac8da" - integrity sha512-6EUEGzEg4acwowvgR/yVZIj8S2Kkwc6JmlY2/wnM1wJHNz20o7s1TIGrxnah8ymLgJasYDpy95P3TMMqlmetPw== + version "2.0.7" + resolved "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.7.tgz#0d97b7a886a0c0d2158682145cd949b29f8efe86" + integrity sha512-0HPVSqDmOWk5fRLb+bqGal+6iWsERiEco/Mli77yy5NEy22IfkoRoqZTSZ8UtXDWY9DCZlpS1Jqq56iWx2torw== dependencies: "@types/express" "*" "@types/passport" "*" From a1351acc87647697b59604d7f34832af2c3140d8 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Fri, 30 Apr 2021 13:46:35 +0200 Subject: [PATCH 18/18] awsS3 option endpoint -> s3ForcePathStyle Signed-off-by: Raghunandan --- plugins/techdocs-backend/config.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/techdocs-backend/config.d.ts b/plugins/techdocs-backend/config.d.ts index 57dbb5108e..6a76172638 100644 --- a/plugins/techdocs-backend/config.d.ts +++ b/plugins/techdocs-backend/config.d.ts @@ -96,7 +96,7 @@ export interface Config { * This allows providers like LocalStack, Minio and Wasabi (and possibly others) to be used to host tech docs. * @visibility backend */ - endpoint?: string; + s3ForcePathStyle?: boolean; }; } | {