From e3adec2bd1af2345fa498a7da89945a7ac8e065b Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Mon, 22 Feb 2021 20:14:08 -0800 Subject: [PATCH 001/147] feature: add support for apps to pass in a KubernetesClustersSupplier --- .changeset/lucky-turtles-sort.md | 5 +++++ .../kubernetes-backend/src/service/router.ts | 17 +++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 .changeset/lucky-turtles-sort.md diff --git a/.changeset/lucky-turtles-sort.md b/.changeset/lucky-turtles-sort.md new file mode 100644 index 0000000000..56c223da2a --- /dev/null +++ b/.changeset/lucky-turtles-sort.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Allow apps to pass in a KubernetesClustersSupplier diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts index 32b6dfb832..4545351dcc 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -27,6 +27,7 @@ import { ServiceLocatorMethod, ClusterLocatorMethod, ClusterDetails, + KubernetesClustersSupplier, } from '..'; import { getCombinedClusterDetails } from '../cluster-locator'; import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; @@ -34,6 +35,7 @@ import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; export interface RouterOptions { logger: Logger; config: Config; + clusterSupplier?: KubernetesClustersSupplier; } const getServiceLocator = ( @@ -98,10 +100,17 @@ export async function createRouter( 'kubernetes.clusterLocatorMethods', ) as ClusterLocatorMethod[]; - const clusterDetails = await getCombinedClusterDetails( - clusterLocatorMethods, - options.config, - ); + let clusterDetails: ClusterDetails[]; + + if (options.clusterSupplier) { + clusterDetails = await options.clusterSupplier.getClusters(); + // Append supplied clusters to cluster retrieved via getCombinedClusterDetails + } else { + clusterDetails = await getCombinedClusterDetails( + clusterLocatorMethods, + options.config, + ); + } const serviceLocator = getServiceLocator(options.config, clusterDetails); From 8d4f5ee0ac437fecb2d1ef678a80d4b9575c2cea Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Sat, 27 Feb 2021 12:13:11 -0800 Subject: [PATCH 002/147] Remove incorrect comment --- plugins/kubernetes-backend/src/service/router.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts index 4545351dcc..fe79b3a1a0 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -104,7 +104,6 @@ export async function createRouter( if (options.clusterSupplier) { clusterDetails = await options.clusterSupplier.getClusters(); - // Append supplied clusters to cluster retrieved via getCombinedClusterDetails } else { clusterDetails = await getCombinedClusterDetails( clusterLocatorMethods, From dbea11072f48b5543830aa38cd3afc4b3a099ddb Mon Sep 17 00:00:00 2001 From: Tadashi Nemoto Date: Mon, 1 Mar 2021 19:48:25 +0900 Subject: [PATCH 003/147] [Lighthouse plugin] Change setup wording of intro card to latest --- .changeset/yellow-rice-appear.md | 5 +++ .../lighthouse/src/components/Intro/index.tsx | 36 +++++++++++-------- 2 files changed, 26 insertions(+), 15 deletions(-) create mode 100644 .changeset/yellow-rice-appear.md diff --git a/.changeset/yellow-rice-appear.md b/.changeset/yellow-rice-appear.md new file mode 100644 index 0000000000..209b379bb6 --- /dev/null +++ b/.changeset/yellow-rice-appear.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-lighthouse': patch +--- + +Change setup wording of intro card to latest diff --git a/plugins/lighthouse/src/components/Intro/index.tsx b/plugins/lighthouse/src/components/Intro/index.tsx index b140141c9f..8e8f9ac88f 100644 --- a/plugins/lighthouse/src/components/Intro/index.tsx +++ b/plugins/lighthouse/src/components/Intro/index.tsx @@ -41,31 +41,37 @@ To get started, you will need a running instance of [lighthouse-audit-service](h _It's likely you will need to enable CORS when running lighthouse-audit-service. Initialize the app with the environment variable \`LAS_CORS\` set to \`true\`._ -When you have an instance running that Backstage can hook into, make sure to export the plugin in -your app's [\`plugins.ts\`](https://github.com/backstage/backstage/blob/master/packages/app/src/plugins.ts) -to enable the plugin: +When you have an instance running that Backstage can hook into, first install the plugin into your app: + +\`\`\`sh +$ yarn add @backstage/plugin-lighthouse +\`\`\` + +Then make sure to export the plugin in your app's [\`plugins.ts\`](https://github.com/backstage/backstage/blob/master/packages/app/src/plugins.ts) to enable the plugin: \`\`\`js export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse'; \`\`\` -Then, you need to use the \`lighthouseApiRef\` exported from the plugin to initialize the Rest API in -your [\`apis.ts\`](https://github.com/backstage/backstage/blob/master/packages/app/src/apis.ts). +Modify your app routes in \`App.tsx\` to include the Router component exported from the plugin, for example: \`\`\`js -import { ApiHolder, ApiRegistry } from '@backstage/core'; -import { - lighthouseApiRef, - LighthouseRestApi, -} from '@backstage/plugin-lighthouse'; +// At the top imports +import { Router as LighthouseRouter } from '@backstage/plugin-lighthouse'; -const builder = ApiRegistry.builder(); +// Inside App component + + // ... + } /> + // ... +; +\`\`\` -export const lighthouseApi = - new LighthouseRestApi(/* your service url here! */); -builder.add(lighthouseApiRef, lighthouseApi); +Then configure the \`lighthouse-audit-service\` URL in your [\`app-config.yaml\`](https://github.com/backstage/backstage/blob/master/app-config.yaml). -export default builder.build() as ApiHolder; +\`\`\`yaml +lighthouse: + baseUrl: http://your-service-url \`\`\` `; From 86b0d852d9b46a0f6590a014f1d2824d9476d6f1 Mon Sep 17 00:00:00 2001 From: Tadashi Nemoto Date: Tue, 2 Mar 2021 11:10:20 +0900 Subject: [PATCH 004/147] Modify to latest by feedback --- plugins/lighthouse/src/components/Intro/index.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/lighthouse/src/components/Intro/index.tsx b/plugins/lighthouse/src/components/Intro/index.tsx index 8e8f9ac88f..1756f441a5 100644 --- a/plugins/lighthouse/src/components/Intro/index.tsx +++ b/plugins/lighthouse/src/components/Intro/index.tsx @@ -53,18 +53,18 @@ Then make sure to export the plugin in your app's [\`plugins.ts\`](https://githu export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse'; \`\`\` -Modify your app routes in \`App.tsx\` to include the Router component exported from the plugin, for example: +Modify your app routes in \`App.tsx\` or \`App.jsx\` to include the Router component exported from the plugin, for example: \`\`\`js // At the top imports -import { Router as LighthouseRouter } from '@backstage/plugin-lighthouse'; +import { LighthousePage } from '@backstage/plugin-lighthouse'; // Inside App component - + // ... - } /> + } /> // ... -; +; \`\`\` Then configure the \`lighthouse-audit-service\` URL in your [\`app-config.yaml\`](https://github.com/backstage/backstage/blob/master/app-config.yaml). From a2d953269845c08c64f04d279f909b894ff79154 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Mar 2021 04:06:53 +0000 Subject: [PATCH 005/147] chore(deps): bump react-hook-form from 6.6.0 to 6.15.4 Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 6.6.0 to 6.15.4. - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v6.6.0...v6.15.4) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index d5c45157e3..fb78a92355 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21325,9 +21325,9 @@ react-helmet@6.1.0: react-side-effect "^2.1.0" react-hook-form@^6.6.0: - version "6.6.0" - resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.6.0.tgz#bda76fc50a916096afe18119f6f57f2b3911cdb7" - integrity sha512-5UKx2SBMaWuwy614H730Vv2QmoGdsdYpN96MnWXYzC8cHIUi9N2hYwtI86TEIcBHJ6FvEewh3onfA7P243RM1g== + version "6.15.4" + resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.4.tgz#328003e1ccc096cd158899ffe7e3b33735a9b024" + integrity sha512-K+Sw33DtTMengs8OdqFJI3glzNl1wBzSefD/ksQw/hJf9CnOHQAU6qy82eOrh0IRNt2G53sjr7qnnw1JDjvx1w== react-hot-loader@^4.12.21: version "4.12.21" From e3653bf66d96656385f21ebdc115a32412c2a8e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Mar 2021 04:07:47 +0000 Subject: [PATCH 006/147] chore(deps-dev): bump @types/webpack-dev-server from 3.11.0 to 3.11.1 Bumps [@types/webpack-dev-server](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/webpack-dev-server) from 3.11.0 to 3.11.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/webpack-dev-server) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index d5c45157e3..beb3a8ee81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6797,9 +6797,9 @@ integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ== "@types/webpack-dev-server@*", "@types/webpack-dev-server@^3.11.0": - version "3.11.0" - resolved "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz#bcc3b85e7dc6ac2db25330610513f2228c2fcfb2" - integrity sha512-3+86AgSzl18n5P1iUP9/lz3G3GMztCp+wxdDvVuNhx1sr1jE79GpYfKHL8k+Vht3N74K2n98CuAEw4YPJCYtDA== + version "3.11.1" + resolved "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.11.1.tgz#f8f4dac1da226d530bd15a1d5dc34b23ba766ccb" + integrity sha512-rIb+LtUkKnh7+oIJm3WiMJONd71Q0lZuqGLcSqhZ5qjN9gV/CNmZe7Bai+brnBPZ/KVYOsr+4bFLiNZwjBicLw== dependencies: "@types/connect-history-api-fallback" "*" "@types/express" "*" From be7acb11e003683d881117b015d514271277b7a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Mar 2021 04:08:36 +0000 Subject: [PATCH 007/147] chore(deps-dev): bump @types/diff from 4.0.2 to 5.0.0 Bumps [@types/diff](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/diff) from 4.0.2 to 5.0.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/diff) 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 1018d05221..cdf51d6df5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -123,7 +123,7 @@ "@backstage/dev-utils": "^0.1.12", "@backstage/test-utils": "^0.1.8", "@backstage/theme": "^0.2.3", - "@types/diff": "^4.0.2", + "@types/diff": "^5.0.0", "@types/express": "^4.17.6", "@types/fs-extra": "^9.0.1", "@types/html-webpack-plugin": "^3.2.2", diff --git a/yarn.lock b/yarn.lock index d5c45157e3..50bf8c21b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5838,10 +5838,10 @@ resolved "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.44.tgz#8f4b796b118ca29c132da7068fbc0d0351ee5851" integrity sha512-N6HD+79w77ZVAaVO7JJDW5yJ9LAxM62FpgNGO9xEde+KVYjDRyhIMzfiErXpr1g0JPon9kwlBzoBK6s4fOww9Q== -"@types/diff@^4.0.2": - version "4.0.2" - resolved "https://registry.npmjs.org/@types/diff/-/diff-4.0.2.tgz#2e9bb89f9acc3ab0108f0f3dc4dbdcf2fff8a99c" - integrity sha512-mIenTfsIe586/yzsyfql69KRnA75S8SVXQbTLpDejRrjH0QSJcpu3AUOi/Vjnt9IOsXKxPhJfGpQUNMueIU1fQ== +"@types/diff@^5.0.0": + version "5.0.0" + resolved "https://registry.npmjs.org/@types/diff/-/diff-5.0.0.tgz#eb71e94feae62548282c4889308a3dfb57e36020" + integrity sha512-jrm2K65CokCCX4NmowtA+MfXyuprZC13jbRuwprs6/04z/EcFg/MCwYdsHn+zgV4CQBiATiI7AEq7y1sZCtWKA== "@types/dockerode@^3.2.1": version "3.2.1" From 314fa2b817d44b9eb9ce59257c314e96ab9a8c79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Mar 2021 04:10:59 +0000 Subject: [PATCH 008/147] chore(deps): bump color from 3.1.2 to 3.1.3 Bumps [color](https://github.com/Qix-/color) from 3.1.2 to 3.1.3. - [Release notes](https://github.com/Qix-/color/releases) - [Commits](https://github.com/Qix-/color/compare/3.1.2...3.1.3) Signed-off-by: dependabot[bot] --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index d5c45157e3..3c6cbb4bb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9729,10 +9729,10 @@ color-name@^1.0.0, color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.5.2: - version "1.5.3" - resolved "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" - integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== +color-string@^1.5.2, color-string@^1.5.4: + version "1.5.4" + resolved "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6" + integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw== dependencies: color-name "^1.0.0" simple-swizzle "^0.2.2" @@ -9746,12 +9746,12 @@ color@3.0.x: color-string "^1.5.2" color@^3.0.0, color@^3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10" - integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg== + version "3.1.3" + resolved "https://registry.npmjs.org/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" + integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ== dependencies: color-convert "^1.9.1" - color-string "^1.5.2" + color-string "^1.5.4" colorette@1.2.1, colorette@^1.2.1: version "1.2.1" From bece0905722440bbb43a9d05021181985b00f493 Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Mon, 1 Mar 2021 17:10:42 +0100 Subject: [PATCH 009/147] Improve rendering of multiple support item links in the SupportButton Signed-off-by: Dominik Henneke --- .changeset/rude-ways-camp.md | 5 +++++ .../src/components/SupportButton/SupportButton.tsx | 12 ++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .changeset/rude-ways-camp.md diff --git a/.changeset/rude-ways-camp.md b/.changeset/rude-ways-camp.md new file mode 100644 index 0000000000..37413141bd --- /dev/null +++ b/.changeset/rude-ways-camp.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Improve rendering of multiple support item links in the `SupportButton` diff --git a/packages/core/src/components/SupportButton/SupportButton.tsx b/packages/core/src/components/SupportButton/SupportButton.tsx index 7c532c0f8f..09bd27de2f 100644 --- a/packages/core/src/components/SupportButton/SupportButton.tsx +++ b/packages/core/src/components/SupportButton/SupportButton.tsx @@ -66,12 +66,12 @@ const SupportListItem = ({ item }: { item: SupportItem }) => { - {item.links && - item.links.map(link => ( - - ))} - + item.links && + item.links + .map(link => ( + + )) + .reduce((prev, curr, idx) => [prev,
, curr]) } /> From 05183f2028f77262f6c7a1c7def0f74ed466922c Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Tue, 2 Mar 2021 14:22:43 +0100 Subject: [PATCH 010/147] Fix incompatility of the catalog-import plugin with the react-hook-form v6.15.4 Signed-off-by: Dominik Henneke --- .changeset/polite-ads-press.md | 5 +++++ plugins/catalog-import/package.json | 2 +- .../StepPrepareCreatePullRequest/AutocompleteTextField.tsx | 4 ++-- .../PreparePullRequestForm.test.tsx | 6 +++--- yarn.lock | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 .changeset/polite-ads-press.md diff --git a/.changeset/polite-ads-press.md b/.changeset/polite-ads-press.md new file mode 100644 index 0000000000..6e9ba2f5c3 --- /dev/null +++ b/.changeset/polite-ads-press.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-import': patch +--- + +Bump react-hook-form version constraint to "^6.15.4" diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 83c9a0394e..517584b9d6 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -44,7 +44,7 @@ "git-url-parse": "^11.4.4", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-hook-form": "^6.6.0", + "react-hook-form": "^6.15.4", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^15.3.3", diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx index 3231c3c6a1..a3743dc580 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx @@ -22,7 +22,7 @@ import { Control, Controller, FieldErrors, - ValidationRules, + UseControllerOptions, } from 'react-hook-form'; type Props = { @@ -32,7 +32,7 @@ type Props = { control?: Control>; errors?: FieldErrors>; - rules?: ValidationRules; + rules?: UseControllerOptions>['rules']; loading?: boolean; loadingText?: string; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx index 0bf7746e6a..4693535d24 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx @@ -25,7 +25,7 @@ describe('', () => { const onSubmitFn = jest.fn(); const { getByRole } = render( - defaultValues={{ main: 'default' }} render={({ register }) => ( <> @@ -49,7 +49,7 @@ describe('', () => { const onSubmitFn = jest.fn(); const { getByRole, getByLabelText } = render( - defaultValues={{ main: 'default' }} render={({ register }) => ( <> @@ -80,7 +80,7 @@ describe('', () => { const onSubmitFn = jest.fn(); const { queryByText, getByRole } = render( - defaultValues={{}} render={({ errors, register }) => ( <> diff --git a/yarn.lock b/yarn.lock index fb78a92355..41a2ba3934 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21324,7 +21324,7 @@ react-helmet@6.1.0: react-fast-compare "^3.1.1" react-side-effect "^2.1.0" -react-hook-form@^6.6.0: +react-hook-form@^6.15.4, react-hook-form@^6.6.0: version "6.15.4" resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.4.tgz#328003e1ccc096cd158899ffe7e3b33735a9b024" integrity sha512-K+Sw33DtTMengs8OdqFJI3glzNl1wBzSefD/ksQw/hJf9CnOHQAU6qy82eOrh0IRNt2G53sjr7qnnw1JDjvx1w== From 60dcadeef6b791191e4865d7c67c2d968c863ee3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Mar 2021 20:56:40 +0100 Subject: [PATCH 011/147] scaffolder-backend: fix action tests on windows Signed-off-by: Patrik Oldsberg --- .../scaffolder/actions/builtin/fetch/cookiecutter.test.ts | 6 +++++- .../src/scaffolder/actions/builtin/fetch/helpers.test.ts | 5 +++-- .../src/scaffolder/actions/builtin/fetch/plain.test.ts | 3 ++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts index 7d26bcb20a..0de7d94fda 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.test.ts @@ -16,6 +16,7 @@ jest.mock('./helpers'); import os from 'os'; +import { resolve as resolvePath } from 'path'; import { createFetchCookiecutterAction } from './cookiecutter'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; @@ -89,7 +90,10 @@ describe('fetch:cookiecutter', () => { integrations, baseUrl: mockContext.baseUrl, fetchUrl: mockContext.input.url, - outputPath: `${mockContext.workspacePath}/template/{{cookiecutter and 'contents'}}`, + outputPath: resolvePath( + mockContext.workspacePath, + `template/{{cookiecutter and 'contents'}}`, + ), }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.test.ts index e475764a0b..c1e022b8b9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.test.ts @@ -15,8 +15,9 @@ */ jest.mock('fs-extra'); -import fs from 'fs-extra'; +import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; import { UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -77,7 +78,7 @@ describe('fetchContent helper', () => { fetchUrl: 'foo', outputPath: 'somepath', }); - expect(fs.copy).toBeCalledWith('/some/foo', 'somepath'); + expect(fs.copy).toBeCalledWith(resolvePath('/some/foo'), 'somepath'); }); it('should reject if no integration matches location', async () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts index 88eeb5997b..97c74d6187 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts @@ -16,6 +16,7 @@ jest.mock('./helpers'); import os from 'os'; +import { resolve as resolvePath } from 'path'; import { getVoidLogger, UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -76,7 +77,7 @@ describe('fetch:plain', () => { }); expect(fetchContents).toBeCalledWith( expect.objectContaining({ - outputPath: `${mockContext.workspacePath}/lol`, + outputPath: resolvePath(mockContext.workspacePath, 'lol'), fetchUrl: 'https://github.com/backstage/community/tree/main/backstage-community-sessions/assets', }), From 11c6208fe2580d58a2e46f139ae99857418e87db Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 3 Mar 2021 01:12:18 +0100 Subject: [PATCH 012/147] core-api: workaround for backend:dev port conflict issue Signed-off-by: Patrik Oldsberg --- .changeset/perfect-scissors-swim.md | 5 +++++ packages/cli/src/commands/backend/dev.ts | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 .changeset/perfect-scissors-swim.md diff --git a/.changeset/perfect-scissors-swim.md b/.changeset/perfect-scissors-swim.md new file mode 100644 index 0000000000..7cbe02d321 --- /dev/null +++ b/.changeset/perfect-scissors-swim.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fixed an issue where the `backend:dev` command would get stuck executing the backend process multiple times, causing port conflict issues. diff --git a/packages/cli/src/commands/backend/dev.ts b/packages/cli/src/commands/backend/dev.ts index 88c069a92b..395fe8a4ab 100644 --- a/packages/cli/src/commands/backend/dev.ts +++ b/packages/cli/src/commands/backend/dev.ts @@ -14,10 +14,17 @@ * limitations under the License. */ +import fs from 'fs-extra'; import { Command } from 'commander'; +import { paths } from '../../lib/paths'; import { serveBackend } from '../../lib/bundler/backend'; export default async (cmd: Command) => { + // Cleaning dist/ before we start the dev process helps work around an issue + // where we end up with the entrypoint executing multiple times, causing + // a port bind conflict among other things. + await fs.remove(paths.resolveTarget('dist')); + const waitForExit = await serveBackend({ entry: 'src/index', checksEnabled: cmd.check, From 4703b217d74f7a26100ab1061b0300ebe1daf803 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Tue, 2 Mar 2021 22:50:30 -0500 Subject: [PATCH 013/147] Add reference to theming Signed-off-by: Adam Harvey --- docs/dls/design.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/dls/design.md b/docs/dls/design.md index 042312d0a3..a2091aeb10 100644 --- a/docs/dls/design.md +++ b/docs/dls/design.md @@ -127,6 +127,10 @@ your own plugin for Backstage. **[Discord](https://discord.gg/EBHEGzX)** - all design questions should be directed to the _#design_ channel. +**[Customize Backstage's look and feel](https://backstage.io/docs/getting-started/app-custom-theme)** - +How to customize the look and feel of your Backstage instance by extending the +theme. + ## 🔮 Future ### Contributions from designers From b9d786ed04361da752c2d141acf1a0164eba916c Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Tue, 2 Mar 2021 22:50:43 -0500 Subject: [PATCH 014/147] Fix metadata Signed-off-by: Adam Harvey --- docs/features/software-templates/legacy.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/features/software-templates/legacy.md b/docs/features/software-templates/legacy.md index d70773e251..49f3e21904 100644 --- a/docs/features/software-templates/legacy.md +++ b/docs/features/software-templates/legacy.md @@ -1,9 +1,8 @@ --- id: template-legacy title: Writing Templates (Legacy) -description: - Old documentation describing the backstage.io/v1alpha1 format of the Template - Schema +# prettier-ignore +description: Old documentation describing the backstage.io/v1alpha1 format of the Template Schema --- ## Kind: Template From 461fc63b093c05d08181732f7f68a7ec26c2c0c7 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Tue, 2 Mar 2021 22:51:07 -0500 Subject: [PATCH 015/147] Enhance description Signed-off-by: Adam Harvey --- docs/dls/contributing-to-storybook.md | 36 ++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/docs/dls/contributing-to-storybook.md b/docs/dls/contributing-to-storybook.md index 058f4a23a7..41d2b56ec2 100644 --- a/docs/dls/contributing-to-storybook.md +++ b/docs/dls/contributing-to-storybook.md @@ -4,8 +4,38 @@ title: Contributing to Storybook description: Documentation on How to Contribute to Storybook --- +Storybook is an open source tool to provide users a way to explore the Backstage +UI elements and how to use them in developing the Backstage core and its +plugins. These UI elements are commonly called "components", and include things +such as buttons, tables, specialized widgets with specific formatting, and so +on. + You find our storybook at -[http://backstage.io/storybook](http://backstage.io/storybook) +[http://backstage.io/storybook](http://backstage.io/storybook). + +As noted in the [design introduction](design.md), Backstage's design is based +off of [Material-UI](https://material-ui.com/). Much of the UI elements use +direct Material-UI, while we've also extended and written custom ones to provide +specific functionality. + +![Storybook Page](../assets/dls/storybook-page.png) + +Storybook "Stories" are used to provide examples for how to use a specific +component, which are then displayed both visually and with sample code to be +copied. + +When custom Backstage components are created, they are placed in the +`@backstage/core` package and added to the Storybook. + +There may be times where an existing Material-UI component (in +`@material-ui/core`) is sufficient and doesn't need to be wrapped or duplicated. +However, we may want to identify an _opinionated_ way to use that component +inside of Backstage. In these cases, stories showing how to use those existing +components will also be put into our storybook. + +When a story example using Material-UI becomes more complex, requiring a +specific set of colors, variants, parameters, etc., it may become a candidate to +be refactored to become a full Backstage core component. ## Creating a new Story @@ -33,12 +63,10 @@ core Go to `packages/storybook`, run `yarn install` and install the dependencies, then run the following on your command line: `yarn start` -![](../assets/dls/running-storybook.png) +![Storybook command startup](../assets/dls/running-storybook.png) _You should see a log like the image above._ If everything worked out, your server will be running on **port 6006**, go to your browser and navigate to `http://localhost:6006/`. You should be able to navigate and see the Storybook page. - -![](../assets/dls/storybook-page.png) From 8d8b3087f1964e12315f33d99f870a97d23ac9e7 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Tue, 2 Mar 2021 22:51:25 -0500 Subject: [PATCH 016/147] Update to latest image Signed-off-by: Adam Harvey --- docs/assets/dls/storybook-page.png | Bin 48002 -> 251736 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/assets/dls/storybook-page.png b/docs/assets/dls/storybook-page.png index 58470feaff2e1d68876a00960f36abfc812a0803..b1b98f13385bb60bac2d137dc953155ff802ea98 100644 GIT binary patch literal 251736 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 Jw%SgQ{{zA=6vzMo From b3d380033036b45b4d13479a780746c4038c0563 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Tue, 2 Mar 2021 23:04:33 -0500 Subject: [PATCH 017/147] Tweak link wording Signed-off-by: Adam Harvey --- docs/dls/contributing-to-storybook.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dls/contributing-to-storybook.md b/docs/dls/contributing-to-storybook.md index 41d2b56ec2..26efd30eda 100644 --- a/docs/dls/contributing-to-storybook.md +++ b/docs/dls/contributing-to-storybook.md @@ -10,7 +10,7 @@ plugins. These UI elements are commonly called "components", and include things such as buttons, tables, specialized widgets with specific formatting, and so on. -You find our storybook at +You'll find our storybook at [http://backstage.io/storybook](http://backstage.io/storybook). As noted in the [design introduction](design.md), Backstage's design is based From ea58f5cdd7b5435878fde4d7627602c6fa85f953 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Mar 2021 04:07:08 +0000 Subject: [PATCH 018/147] chore(deps): bump rc-progress from 3.1.1 to 3.1.3 Bumps [rc-progress](https://github.com/react-component/progress) from 3.1.1 to 3.1.3. - [Release notes](https://github.com/react-component/progress/releases) - [Changelog](https://github.com/react-component/progress/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-component/progress/compare/v3.1.1...v3.1.3) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index b45e76220a..64c1b68204 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21130,9 +21130,9 @@ raw-loader@^4.0.1: schema-utils "^2.6.5" rc-progress@^3.0.0: - version "3.1.1" - resolved "https://registry.npmjs.org/rc-progress/-/rc-progress-3.1.1.tgz#f7ba5eb33cc438862513ab5a71739feaf6b3ef4f" - integrity sha512-1ns3pW7ll9bHfdXtlVLF+vngdvlxiCDtiqwXnZFEdurst11JTiPxVdeqnCNbhWx5hP4kCKkAPqG1N0FVfTSUGA== + version "3.1.3" + resolved "https://registry.npmjs.org/rc-progress/-/rc-progress-3.1.3.tgz#d77d8fd26d9d948d72c2a28b64b71a6e86df2426" + integrity sha512-Jl4fzbBExHYMoC6HBPzel0a9VmhcSXx24LVt/mdhDM90MuzoMCJjXZAlhA0V0CJi+SKjMhfBoIQ6Lla1nD4QNw== dependencies: "@babel/runtime" "^7.10.1" classnames "^2.2.6" From 61cece30caab8b63e1ecd1092ec8cdfeee144299 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Mar 2021 04:08:06 +0000 Subject: [PATCH 019/147] chore(deps): bump ts-loader from 7.0.4 to 8.0.17 Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 7.0.4 to 8.0.17. - [Release notes](https://github.com/TypeStrong/ts-loader/releases) - [Changelog](https://github.com/TypeStrong/ts-loader/blob/master/CHANGELOG.md) - [Commits](https://github.com/TypeStrong/ts-loader/compare/v7.0.4...v8.0.17) Signed-off-by: dependabot[bot] --- packages/cli/package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index cdf51d6df5..7285f3dd9c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -106,7 +106,7 @@ "tar": "^6.0.1", "terser-webpack-plugin": "^1.4.3", "ts-jest": "^26.4.3", - "ts-loader": "^7.0.4", + "ts-loader": "^8.0.17", "typescript": "^4.0.3", "url-loader": "^4.1.0", "webpack": "^4.41.6", diff --git a/yarn.lock b/yarn.lock index b45e76220a..5cf4a998e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9290,7 +9290,7 @@ chainsaw@~0.1.0: dependencies: traverse ">=0.3.0 <0.4" -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -17110,7 +17110,7 @@ loader-utils@1.2.3: emojis-list "^2.0.0" json5 "^1.0.1" -loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0: +loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== @@ -24666,16 +24666,16 @@ ts-jest@^26.4.3: semver "7.x" yargs-parser "20.x" -ts-loader@^7.0.4: - version "7.0.4" - resolved "https://registry.npmjs.org/ts-loader/-/ts-loader-7.0.4.tgz#5d9b95227de5afb91fdd9668f8920eb193cfe0cc" - integrity sha512-5du6OQHl+4ZjO4crEyoYUyWSrmmo7bAO+inkaILZ68mvahqrfoa4nn0DRmpQ4ruT4l+cuJCgF0xD7SBIyLeeow== +ts-loader@^8.0.17: + version "8.0.17" + resolved "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.17.tgz#98f2ccff9130074f4079fd89b946b4c637b1f2fc" + integrity sha512-OeVfSshx6ot/TCxRwpBHQ/4lRzfgyTkvi7ghDVrLXOHzTbSK413ROgu/xNqM72i3AFeAIJgQy78FwSMKmOW68w== dependencies: - chalk "^2.3.0" + chalk "^4.1.0" enhanced-resolve "^4.0.0" - loader-utils "^1.0.2" + loader-utils "^2.0.0" micromatch "^4.0.0" - semver "^6.0.0" + semver "^7.3.4" ts-log@^2.2.3: version "2.2.3" From 6ed97ab3d5f92233909632a39b9bf27bc9501a52 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Mar 2021 04:10:37 +0000 Subject: [PATCH 020/147] chore(deps): bump @octokit/graphql from 4.5.8 to 4.6.0 Bumps [@octokit/graphql](https://github.com/octokit/graphql.js) from 4.5.8 to 4.6.0. - [Release notes](https://github.com/octokit/graphql.js/releases) - [Commits](https://github.com/octokit/graphql.js/compare/v4.5.8...v4.6.0) Signed-off-by: dependabot[bot] --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index b45e76220a..992b395d46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4106,12 +4106,12 @@ universal-user-agent "^5.0.0" "@octokit/graphql@^4.5.8": - version "4.5.8" - resolved "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.5.8.tgz#d42373633c3015d0eafce64a8ce196be167fdd9b" - integrity sha512-WnCtNXWOrupfPJgXe+vSmprZJUr0VIu14G58PMlkWGj3cH+KLZEfKMmbUQ6C3Wwx6fdhzVW1CD5RTnBdUHxhhA== + version "4.6.0" + resolved "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.6.0.tgz#f9abca55f82183964a33439d5264674c701c3327" + integrity sha512-CJ6n7izLFXLvPZaWzCQDjU/RP+vHiZmWdOunaCS87v+2jxMsW9FB5ktfIxybRBxZjxuJGRnxk7xJecWTVxFUYQ== dependencies: "@octokit/request" "^5.3.0" - "@octokit/types" "^6.0.0" + "@octokit/types" "^6.0.3" universal-user-agent "^6.0.0" "@octokit/openapi-types@^2.2.0": From 9d4beed251389e3f8cf07f6a3787e38ca65db218 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Mar 2021 04:11:30 +0000 Subject: [PATCH 021/147] chore(deps): bump eslint-plugin-react from 7.21.5 to 7.22.0 Bumps [eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react) from 7.21.5 to 7.22.0. - [Release notes](https://github.com/yannickcr/eslint-plugin-react/releases) - [Changelog](https://github.com/yannickcr/eslint-plugin-react/blob/master/CHANGELOG.md) - [Commits](https://github.com/yannickcr/eslint-plugin-react/compare/v7.21.5...v7.22.0) Signed-off-by: dependabot[bot] --- yarn.lock | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/yarn.lock b/yarn.lock index b45e76220a..58a1f7887c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7869,16 +7869,7 @@ array.prototype.flat@^1.2.1, array.prototype.flat@^1.2.3: define-properties "^1.1.3" es-abstract "^1.17.0-next.1" -array.prototype.flatmap@^1.2.1, array.prototype.flatmap@^1.2.3: - version "1.2.3" - resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.3.tgz#1c13f84a178566042dd63de4414440db9222e443" - integrity sha512-OOEk+lkePcg+ODXIpvuU9PAryCikCJyo7GlDG1upleEpQRx6mzL9puEBkozQ5iAx20KV0l3DbyQwqciJtqe5Pg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" - -array.prototype.flatmap@^1.2.4: +array.prototype.flatmap@^1.2.1, array.prototype.flatmap@^1.2.3, array.prototype.flatmap@^1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz#94cfd47cc1556ec0747d97f7c7738c58122004c9" integrity sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q== @@ -12215,9 +12206,9 @@ eslint-plugin-react-hooks@^4.0.0: integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ== eslint-plugin-react@^7.12.4: - version "7.21.5" - resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.21.5.tgz#50b21a412b9574bfe05b21db176e8b7b3b15bff3" - integrity sha512-8MaEggC2et0wSF6bUeywF7qQ46ER81irOdWS4QWxnnlAEsnzeBevk1sWh7fhpCghPpXb+8Ks7hvaft6L/xsR6g== + version "7.22.0" + resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.22.0.tgz#3d1c542d1d3169c45421c1215d9470e341707269" + integrity sha512-p30tuX3VS+NWv9nQot9xIGAHBXR0+xJVaZriEsHoJrASGCJZDJ8JLNM0YqKqI0AKm6Uxaa1VUHoNEibxRCMQHA== dependencies: array-includes "^3.1.1" array.prototype.flatmap "^1.2.3" From de313a1c9490de15b9ad1116222da74d5629a8ba Mon Sep 17 00:00:00 2001 From: erdoganoksuz Date: Wed, 24 Feb 2021 12:00:29 +0300 Subject: [PATCH 022/147] swift publisher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can Bilgiç --- .../src/stages/publish/openStackSwift.ts | 15 ++++- .../src/stages/publish/publish.ts | 6 ++ .../src/stages/publish/types.ts | 3 +- plugins/techdocs-backend/config.d.ts | 2 +- plugins/techdocs/config.d.ts | 56 +++++++++++++++++++ 5 files changed, 77 insertions(+), 5 deletions(-) diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.ts index 3331033f66..b174122789 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.ts +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.ts @@ -103,16 +103,19 @@ export class OpenStackSwiftPublish implements PublisherBase { this.logger = logger; } + public myName: string = 'hey'; + /** - * Upload all the files from the generated `directory` to the S3 bucket. + * Upload all the files from the generated `directory` to the OpenStack Swift container. * Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html */ async publish({ entity, directory }: PublishRequest): Promise { + console.log(entity, directory, 'Publish hey'); try { - // Note: S3 manages creation of parent directories if they do not exist. + // Note: OpenStack Swift 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); - + console.log(allFilesToUpload, entity, 'hey'); const limiter = createLimiter(10); const uploadPromises: Array> = []; for (const filePath of allFilesToUpload) { @@ -169,6 +172,8 @@ export class OpenStackSwiftPublish implements PublisherBase { async fetchTechDocsMetadata( entityName: EntityName, ): Promise { + console.log(entityName, 'fetchTechDocsMetadata hey'); + try { return await new Promise(async (resolve, reject) => { const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; @@ -208,6 +213,8 @@ export class OpenStackSwiftPublish implements PublisherBase { return async (req, res) => { // Trim the leading forward slash // filePath example - /default/Component/documented-component/index.html + console.log('docsRouter hey'); + const filePath = req.path.replace(/^\//, ''); // Files with different extensions (CSS, HTML) need to be served with different headers @@ -241,6 +248,8 @@ export class OpenStackSwiftPublish implements PublisherBase { */ async hasDocsBeenGenerated(entity: Entity): Promise { try { + console.log(entity, 'hasDocsBeenGenerated hey'); + const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; this.storageClient.getFile( this.containerName, diff --git a/packages/techdocs-common/src/stages/publish/publish.ts b/packages/techdocs-common/src/stages/publish/publish.ts index a4e33a2d9c..f70a5f7626 100644 --- a/packages/techdocs-common/src/stages/publish/publish.ts +++ b/packages/techdocs-common/src/stages/publish/publish.ts @@ -22,6 +22,7 @@ import { LocalPublish } from './local'; import { GoogleGCSPublish } from './googleStorage'; import { AwsS3Publish } from './awsS3'; import { AzureBlobStoragePublish } from './azureBlobStorage'; +import { OpenStackSwiftPublish } from './openStackSwift'; type factoryOptions = { logger: Logger; @@ -53,6 +54,11 @@ export class Publisher { 'Creating Azure Blob Storage Container publisher for TechDocs', ); return AzureBlobStoragePublish.fromConfig(config, logger); + case 'openStackSwift': + logger.info( + 'Creating OpenStack Swift Container publisher for TechDocs', + ); + return OpenStackSwiftPublish.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 5e953deb81..f0ecb3cb2b 100644 --- a/packages/techdocs-common/src/stages/publish/types.ts +++ b/packages/techdocs-common/src/stages/publish/types.ts @@ -23,7 +23,8 @@ export type PublisherType = | 'local' | 'googleGcs' | 'awsS3' - | 'azureBlobStorage'; + | 'azureBlobStorage' + | 'openStackSwift'; export type PublishRequest = { entity: Entity; diff --git a/plugins/techdocs-backend/config.d.ts b/plugins/techdocs-backend/config.d.ts index 5a323f244a..c704014b14 100644 --- a/plugins/techdocs-backend/config.d.ts +++ b/plugins/techdocs-backend/config.d.ts @@ -33,7 +33,7 @@ export interface Config { * Techdocs publisher information */ publisher: { - type: 'local' | 'googleGcs' | 'awsS3'; + type: 'local' | 'googleGcs' | 'awsS3' | 'openStackSwift'; }; /** diff --git a/plugins/techdocs/config.d.ts b/plugins/techdocs/config.d.ts index 1c49833d68..c4fe4a47db 100644 --- a/plugins/techdocs/config.d.ts +++ b/plugins/techdocs/config.d.ts @@ -85,6 +85,62 @@ export interface Config { region?: string; }; } + | { + type: 'openStackSwift'; + + /** + * Required when 'type' is set to awsS3 + */ + openStackSwift?: { + /** + * (Optional) Credentials used to access a storage bucket. + * If not set, environment variables or aws config file will be used to authenticate. + * @see https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html + * @see https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-shared.html + * @visibility secret + */ + /** + * (Required) Cloud Storage Container Name + * @visibility backend + */ + containerName: string; + /** + * (Required) Root user name + * @visibility backend + */ + username: string; + /** + * (Required) Root user password + * @visibility backend + */ + password: string; // required + /** + * (Required) Auth url sometimes OpenStack uses different port check your OpenStack apis. + * @visibility backend + */ + authUrl: string; + /** + * (Required) Auth version + * @visibility backend + */ + keystoneAuthVersion: string; + /** + * (Required) Domaind Id + * @visibility backend + */ + domainId: string; + /** + * (Required) Domaind Name + * @visibility backend + */ + domainName: 'Default'; + /** + * (Required) Region + * @visibility backend + */ + region: 'earth'; + }; + } | { type: 'azureBlobStorage'; From 0d3f211a24418d6e7d75f41b60e9a1ca43aa92bd Mon Sep 17 00:00:00 2001 From: erdoganoksuz Date: Wed, 24 Feb 2021 12:10:29 +0300 Subject: [PATCH 023/147] console MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can Bilgiç --- .../techdocs-common/src/stages/publish/openStackSwift.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.ts index b174122789..49d7aaee23 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.ts +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.ts @@ -110,12 +110,10 @@ export class OpenStackSwiftPublish implements PublisherBase { * Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html */ async publish({ entity, directory }: PublishRequest): Promise { - console.log(entity, directory, 'Publish hey'); try { // Note: OpenStack Swift 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); - console.log(allFilesToUpload, entity, 'hey'); const limiter = createLimiter(10); const uploadPromises: Array> = []; for (const filePath of allFilesToUpload) { @@ -172,8 +170,6 @@ export class OpenStackSwiftPublish implements PublisherBase { async fetchTechDocsMetadata( entityName: EntityName, ): Promise { - console.log(entityName, 'fetchTechDocsMetadata hey'); - try { return await new Promise(async (resolve, reject) => { const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; @@ -213,7 +209,6 @@ export class OpenStackSwiftPublish implements PublisherBase { return async (req, res) => { // Trim the leading forward slash // filePath example - /default/Component/documented-component/index.html - console.log('docsRouter hey'); const filePath = req.path.replace(/^\//, ''); @@ -248,8 +243,6 @@ export class OpenStackSwiftPublish implements PublisherBase { */ async hasDocsBeenGenerated(entity: Entity): Promise { try { - console.log(entity, 'hasDocsBeenGenerated hey'); - const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; this.storageClient.getFile( this.containerName, From a42972e8f3213d17e177e3cc40f8f1b2a4e9dcf1 Mon Sep 17 00:00:00 2001 From: erdoganoksuz Date: Wed, 24 Feb 2021 12:14:14 +0300 Subject: [PATCH 024/147] remove prop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can Bilgiç --- packages/techdocs-common/src/stages/publish/openStackSwift.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.ts index 49d7aaee23..066e1e56d2 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.ts +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.ts @@ -103,8 +103,6 @@ export class OpenStackSwiftPublish implements PublisherBase { this.logger = logger; } - public myName: string = 'hey'; - /** * Upload all the files from the generated `directory` to the OpenStack Swift container. * Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html From ed3afb80bc7ee04b66e86e3913479c8bdbbbb6bc Mon Sep 17 00:00:00 2001 From: erdoganoksuz Date: Wed, 24 Feb 2021 17:15:37 +0300 Subject: [PATCH 025/147] tests setup added MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can Bilgiç --- .../techdocs-common/src/stages/publish/openStackSwift.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.ts index 066e1e56d2..30314e1109 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.ts +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.ts @@ -15,7 +15,7 @@ */ import { Entity, EntityName } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import pkgcloud from 'pkgcloud'; +import { storage } from 'pkgcloud'; import express from 'express'; import fs from 'fs-extra'; import JSON5 from 'json5'; @@ -57,7 +57,7 @@ export class OpenStackSwiftPublish implements PublisherBase { 'techdocs.publisher.openStackSwift', ); - const storageClient = pkgcloud.storage.createClient({ + const storageClient = storage.createClient({ provider: 'openstack', username: openStackSwiftConfig.getString('username'), password: openStackSwiftConfig.getString('password'), @@ -86,7 +86,6 @@ export class OpenStackSwiftPublish implements PublisherBase { ); logger.error(`from OpenStack client library: ${err.message}`); - throw new Error(); } }); @@ -94,7 +93,7 @@ export class OpenStackSwiftPublish implements PublisherBase { } constructor( - private readonly storageClient: pkgcloud.storage.Client, + private readonly storageClient: storage.Client, private readonly containerName: string, private readonly logger: Logger, ) { From e36ef00fd0ee961e386e224362bbf1ae70040602 Mon Sep 17 00:00:00 2001 From: erdoganoksuz Date: Thu, 25 Feb 2021 11:35:39 +0300 Subject: [PATCH 026/147] router case added and hasDocsBeenGenerated fixed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can Bilgiç --- .../src/stages/publish/openStackSwift.ts | 28 +++++++++++-------- .../techdocs-backend/src/service/router.ts | 1 + 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.ts index 30314e1109..15d8b20332 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.ts +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.ts @@ -107,6 +107,7 @@ export class OpenStackSwiftPublish implements PublisherBase { * Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html */ async publish({ entity, directory }: PublishRequest): Promise { + this.logger.info(`Publish Called hey`); try { // Note: OpenStack Swift manages creation of parent directories if they do not exist. // So collecting path of only the files is good enough. @@ -167,6 +168,7 @@ export class OpenStackSwiftPublish implements PublisherBase { async fetchTechDocsMetadata( entityName: EntityName, ): Promise { + this.logger.info(`fetchTechDocsMetadata Called hey`); try { return await new Promise(async (resolve, reject) => { const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; @@ -204,6 +206,7 @@ export class OpenStackSwiftPublish implements PublisherBase { */ docsRouter(): express.Handler { return async (req, res) => { + this.logger.info(`docsRouter Called hey`); // Trim the leading forward slash // filePath example - /default/Component/documented-component/index.html @@ -240,18 +243,21 @@ export class OpenStackSwiftPublish implements PublisherBase { */ async hasDocsBeenGenerated(entity: Entity): Promise { try { + this.logger.info(`hasDocsBeenGenerated Called hey`); const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; - this.storageClient.getFile( - this.containerName, - `${entityRootDir}/index.html`, - (err: any, file: any) => { - if (!err && file) { - return Promise.resolve(true); - } - return Promise.resolve(false); - }, - ); - return Promise.resolve(true); + + return new Promise(res => { + this.storageClient.getFile( + this.containerName, + `${entityRootDir}/index.html`, + (err: any, file: any) => { + console.log(file); + if (!err && file) { + res(true); + } else res(false); + }, + ); + }); } catch (e) { return Promise.resolve(false); } diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index 7f6ff60fdd..b331d2170c 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -155,6 +155,7 @@ export async function createRouter({ break; case 'awsS3': case 'azureBlobStorage': + case 'openStackSwift': 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. From 75ec0e2358fec86f08ef95b8b9ee716be5d50c47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Can=20Bilgi=C3=A7?= Date: Thu, 25 Feb 2021 10:48:25 +0300 Subject: [PATCH 027/147] mocked openstackswift publisher and created mocks first test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can Bilgiç --- .../techdocs-common/__mocks__/pkgcloud.ts | 94 +++++++++++++++++++ .../src/stages/publish/publish.test.ts | 25 +++++ 2 files changed, 119 insertions(+) create mode 100644 packages/techdocs-common/__mocks__/pkgcloud.ts diff --git a/packages/techdocs-common/__mocks__/pkgcloud.ts b/packages/techdocs-common/__mocks__/pkgcloud.ts new file mode 100644 index 0000000000..3ea3c251e0 --- /dev/null +++ b/packages/techdocs-common/__mocks__/pkgcloud.ts @@ -0,0 +1,94 @@ +/* + * 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 { OpenstackProviderOptions } from 'pkgcloud'; +import fs from 'fs-extra'; +import os from 'os'; +import path from 'path'; + +const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; + +const checkFileExists = async (Key: string): Promise => { + // Key will always have / as file separator irrespective of OS since S3 expects /. + // Normalize Key to OS specific path before checking if file exists. + const relativeFilePath = Key.split(path.posix.sep).join(path.sep); + const filePath = path.join(rootDir, Key); + + try { + await fs.access(filePath, fs.constants.F_OK); + return true; + } catch (err) { + return false; + } +}; + +class PkgCloudStorageClient { + getFile( + containerName: string, + file: string, + callback: (err: string, file: string) => any, + ) { + checkFileExists(file).then(res => { + if (!res) { + callback('File does not exist', undefined); + throw new Error('File does not exist'); + } else { + callback(undefined, 'success'); + } + }); + } + + getContainer( + containerName: string, + callback: (err: string, container: string) => any, + ) { + if (containerName !== 'mock') { + callback("Container doesn't exist", undefined); + throw new Error('Container does not exist'); + } else { + callback(undefined, 'success'); + } + } + + upload({ containerName, remote }: { containerName: string; remote: string }) { + checkFileExists(remote).then(res => { + if (!res) { + return new Error("File doesn't exists"); + } + return fs.createWriteStream(`${containerName}/${remote}`); + }); + } + + download({ + containerName, + remote, + }: { + containerName: string; + remote: string; + }) { + checkFileExists(remote).then(res => { + if (!res) { + return new Error("File doesn't exists"); + } + return fs.createReadStream(remote); + }); + } +} + +export class storage { + static createClient(params: OpenstackProviderOptions) { + return new PkgCloudStorageClient(); + } +} diff --git a/packages/techdocs-common/src/stages/publish/publish.test.ts b/packages/techdocs-common/src/stages/publish/publish.test.ts index d1faf82b78..f9bd0f3934 100644 --- a/packages/techdocs-common/src/stages/publish/publish.test.ts +++ b/packages/techdocs-common/src/stages/publish/publish.test.ts @@ -23,6 +23,7 @@ import { LocalPublish } from './local'; import { GoogleGCSPublish } from './googleStorage'; import { AwsS3Publish } from './awsS3'; import { AzureBlobStoragePublish } from './azureBlobStorage'; +import { OpenStackSwiftPublish } from './openStackSwift'; const logger = getVoidLogger(); const discovery: jest.Mocked = { @@ -161,4 +162,28 @@ describe('Publisher', () => { }); expect(publisher).toBeInstanceOf(AzureBlobStoragePublish); }); + + it('should create Open Stack Swift publisher from config', async () => { + const mockConfig = new ConfigReader({ + techdocs: { + requestUrl: 'http://localhost:7000', + publisher: { + type: 'openStackSwift', + openStackSwift: { + username: 'mockuser', + password: 'verystrongpass', + authUrl: 'mockauthurl', + region: 'mockregion', + containerName: 'mock', + }, + }, + }, + }); + + const publisher = await Publisher.fromConfig(mockConfig, { + logger, + discovery, + }); + expect(publisher).toBeInstanceOf(OpenStackSwiftPublish); + }); }); From 32fac33d8d8e46b8dc90837f9e75bd589edcf88c Mon Sep 17 00:00:00 2001 From: gmzsenturk Date: Thu, 25 Feb 2021 15:29:23 +0300 Subject: [PATCH 028/147] OpenStackSwift tests added to project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can Bilgiç --- .../techdocs-common/__mocks__/pkgcloud.ts | 41 +-- packages/techdocs-common/package.json | 1 + .../src/stages/publish/openStackSwift.test.ts | 248 ++++++++++++++++++ .../src/stages/publish/openStackSwift.ts | 8 +- yarn.lock | 181 +++++++++++-- 5 files changed, 424 insertions(+), 55 deletions(-) create mode 100644 packages/techdocs-common/src/stages/publish/openStackSwift.test.ts diff --git a/packages/techdocs-common/__mocks__/pkgcloud.ts b/packages/techdocs-common/__mocks__/pkgcloud.ts index 3ea3c251e0..bf729a7bd2 100644 --- a/packages/techdocs-common/__mocks__/pkgcloud.ts +++ b/packages/techdocs-common/__mocks__/pkgcloud.ts @@ -13,21 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { OpenstackProviderOptions } from 'pkgcloud'; import fs from 'fs-extra'; import os from 'os'; import path from 'path'; +import { ObjectWritableMock, BufferReadableMock } from 'stream-mock'; const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; const checkFileExists = async (Key: string): Promise => { // Key will always have / as file separator irrespective of OS since S3 expects /. // Normalize Key to OS specific path before checking if file exists. - const relativeFilePath = Key.split(path.posix.sep).join(path.sep); const filePath = path.join(rootDir, Key); try { - await fs.access(filePath, fs.constants.F_OK); + fs.accessSync(filePath, fs.constants.F_OK); return true; } catch (err) { return false; @@ -38,11 +37,11 @@ class PkgCloudStorageClient { getFile( containerName: string, file: string, - callback: (err: string, file: string) => any, + callback: (err: any, file: string) => any, ) { checkFileExists(file).then(res => { if (!res) { - callback('File does not exist', undefined); + callback('File does not exist', file); throw new Error('File does not exist'); } else { callback(undefined, 'success'); @@ -55,40 +54,28 @@ class PkgCloudStorageClient { callback: (err: string, container: string) => any, ) { if (containerName !== 'mock') { - callback("Container doesn't exist", undefined); + callback("Container doesn't exist", containerName); throw new Error('Container does not exist'); } else { - callback(undefined, 'success'); + callback('Container does not exist', 'success'); } } - upload({ containerName, remote }: { containerName: string; remote: string }) { - checkFileExists(remote).then(res => { - if (!res) { - return new Error("File doesn't exists"); - } - return fs.createWriteStream(`${containerName}/${remote}`); - }); + upload() { + return new ObjectWritableMock(); } - download({ - containerName, - remote, - }: { - containerName: string; - remote: string; - }) { - checkFileExists(remote).then(res => { - if (!res) { - return new Error("File doesn't exists"); - } - return fs.createReadStream(remote); + download() { + const stringify = JSON.stringify({ + "site_description": 'site_content', + "site_name": "backstage" }); + return new BufferReadableMock([stringify]); } } export class storage { - static createClient(params: OpenstackProviderOptions) { + static createClient() { return new PkgCloudStorageClient(); } } diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index a543addfa9..2598de1dce 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -58,6 +58,7 @@ "p-limit": "^3.1.0", "pkgcloud": "^2.2.0", "recursive-readdir": "^2.2.2", + "stream-mock": "^2.0.5", "winston": "^3.2.1" }, "devDependencies": { diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts new file mode 100644 index 0000000000..fab0bf3a12 --- /dev/null +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts @@ -0,0 +1,248 @@ +/* + * 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, + ENTITY_DEFAULT_NAMESPACE, +} from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import mockFs from 'mock-fs'; +import os from 'os'; +import path from 'path'; +import * as winston from 'winston'; +import { OpenStackSwiftPublish } from './openStackSwift'; +import { PublisherBase, TechDocsMetadata } from './types'; + +// NOTE: /packages/techdocs-common/__mocks__ is being used to mock pkgcloud client library + +const createMockEntity = (annotations = {}): Entity => { + return { + apiVersion: 'version', + kind: 'TestKind', + metadata: { + name: 'test-component-name', + namespace: 'test-namespace', + annotations: { + ...annotations, + }, + }, + }; +}; + +const createMockEntityName = (): EntityName => ({ + kind: 'TestKind', + name: 'test-component-name', + namespace: 'test-namespace', +}); + +const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; + +const getEntityRootDir = (entity: Entity) => { + const { + kind, + metadata: { namespace, name }, + } = entity; + + return path.join(rootDir, namespace || ENTITY_DEFAULT_NAMESPACE, kind, name); +}; + +const logger = winston.createLogger(); +jest.spyOn(logger, 'info').mockReturnValue(logger); +jest.spyOn(logger, 'error').mockReturnValue(logger); + +let publisher: PublisherBase; + +beforeEach(() => { + mockFs.restore(); + const mockConfig = new ConfigReader({ + techdocs: { + requestUrl: 'http://localhost:7000', + publisher: { + type: 'openStackSwift', + openStackSwift: { + username: 'mockuser', + password: 'verystrongpass', + authUrl: 'mockauthurl', + region: 'mockregion', + containerName: 'mock', + }, + }, + }, + }); + + publisher = OpenStackSwiftPublish.fromConfig(mockConfig, logger); +}); + +describe('OpenStackSwiftPublish', () => { + describe('publish', () => { + beforeEach(() => { + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + mockFs({ + [entityRootDir]: { + 'index.html': '', + '404.html': '', + assets: { + 'main.css': '', + }, + }, + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should publish a directory', async () => { + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + setTimeout(async () => { + expect( + await publisher.publish({ + entity, + directory: entityRootDir, + }), + ).toBeUndefined() + }, 5000); + }); + + it('should fail to publish a directory', async () => { + expect.assertions(3); + const wrongPathToGeneratedDirectory = path.join( + rootDir, + 'wrong', + 'path', + 'to', + 'generatedDirectory', + ); + + const entity = createMockEntity(); + await expect( + publisher.publish({ + entity, + directory: wrongPathToGeneratedDirectory, + }), + ).rejects.toThrowError(); + + await publisher + .publish({ + entity, + directory: wrongPathToGeneratedDirectory, + }) + .catch(error => { + expect(error.message).toEqual( + // Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error + // Issue reported https://github.com/tschaub/mock-fs/issues/118 + expect.stringContaining( + `Unable to upload file(s) to OpenStack Swift. Error: Failed to read template directory: ENOENT, no such file or directory`, + ), + ); + expect(error.message).toEqual( + expect.stringContaining(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); + }); + }); + + describe('fetchTechDocsMetadata', () => { + it('should return tech docs metadata', async () => { + const entityNameMock = createMockEntityName(); + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + mockFs({ + [entityRootDir]: { + 'techdocs_metadata.json': + '{"site_name": "backstage", "site_description": "site_content"}', + }, + }); + + const expectedMetadata: TechDocsMetadata = { + site_name: 'backstage', + site_description: 'site_content', + }; + expect( + await publisher.fetchTechDocsMetadata(entityNameMock), + ).toStrictEqual(expectedMetadata); + mockFs.restore(); + }); + + it('should return tech docs metadata when json encoded with single quotes', async () => { + const entityNameMock = createMockEntityName(); + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + mockFs({ + [entityRootDir]: { + 'techdocs_metadata.json': `{'site_name': 'backstage', 'site_description': 'site_content'}`, + }, + }); + + const expectedMetadata: TechDocsMetadata = { + site_name: 'backstage', + site_description: 'site_content', + }; + expect( + await publisher.fetchTechDocsMetadata(entityNameMock), + ).toStrictEqual(expectedMetadata); + mockFs.restore(); + }); + + it('should return an error if the techdocs_metadata.json file is not present', async () => { + const entityNameMock = createMockEntityName(); + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + await publisher + .fetchTechDocsMetadata(entityNameMock) + .catch(error => + expect(error).toEqual( + new Error( + `TechDocs metadata fetch failed, The file ${path.join( + entityRootDir, + 'techdocs_metadata.json', + )} does not exist !`, + ), + ), + ); + }); + }); +}); diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.ts index 15d8b20332..60c8349dee 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.ts +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.ts @@ -107,7 +107,6 @@ export class OpenStackSwiftPublish implements PublisherBase { * Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html */ async publish({ entity, directory }: PublishRequest): Promise { - this.logger.info(`Publish Called hey`); try { // Note: OpenStack Swift manages creation of parent directories if they do not exist. // So collecting path of only the files is good enough. @@ -139,8 +138,7 @@ export class OpenStackSwiftPublish implements PublisherBase { }; // Rate limit the concurrent execution of file uploads to batches of 10 (per publish) - const uploadFile = limiter( - () => + const uploadFile = limiter(() => new Promise((res, rej) => { const writeStream = this.storageClient.upload(params); @@ -168,7 +166,6 @@ export class OpenStackSwiftPublish implements PublisherBase { async fetchTechDocsMetadata( entityName: EntityName, ): Promise { - this.logger.info(`fetchTechDocsMetadata Called hey`); try { return await new Promise(async (resolve, reject) => { const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; @@ -206,7 +203,6 @@ export class OpenStackSwiftPublish implements PublisherBase { */ docsRouter(): express.Handler { return async (req, res) => { - this.logger.info(`docsRouter Called hey`); // Trim the leading forward slash // filePath example - /default/Component/documented-component/index.html @@ -243,7 +239,6 @@ export class OpenStackSwiftPublish implements PublisherBase { */ async hasDocsBeenGenerated(entity: Entity): Promise { try { - this.logger.info(`hasDocsBeenGenerated Called hey`); const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; return new Promise(res => { @@ -251,7 +246,6 @@ export class OpenStackSwiftPublish implements PublisherBase { this.containerName, `${entityRootDir}/index.html`, (err: any, file: any) => { - console.log(file); if (!err && file) { res(true); } else res(false); diff --git a/yarn.lock b/yarn.lock index f941b3ee7c..5b7893c852 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1816,35 +1816,78 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.2.0": - version "0.7.2" + version "0.2.0" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.2.0.tgz#e3fe2a4ddeb6a9b6ec480c80cb2b9c39cb245576" + integrity sha512-Y1ocdRpBlxK/VrJQjHlQd0bgADECd1B2NRjwd8ss46ibT5hwLvMOfD80+Fa7oPLu0ktJrH4lq0pNIIJIml48zA== dependencies: - "@backstage/config" "^0.1.3" + "@backstage/config" "^0.1.1" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" - ajv "^7.0.3" json-schema "^0.2.5" lodash "^4.17.15" uuid "^8.0.0" yup "^0.29.3" "@backstage/catalog-model@^0.3.0": - version "0.7.2" + version "0.3.1" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.3.1.tgz#45d08e2f333c9c566b2bf2629fd707fe989bb404" + integrity sha512-9XhV7c4rmVW+Yzj2PiwTQ7DsegWGB3C4ELsDRExuEVZONdqNcC02cyJtrt3fT5F31ZS3tHkB9bMUymFOBLqUSA== dependencies: - "@backstage/config" "^0.1.3" + "@backstage/config" "^0.1.1" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" - ajv "^7.0.3" json-schema "^0.2.5" lodash "^4.17.15" uuid "^8.0.0" yup "^0.29.3" "@backstage/core@^0.3.0": - version "0.6.3" + version "0.3.2" + resolved "https://registry.npmjs.org/@backstage/core/-/core-0.3.2.tgz#a8209126d5076cf4a8b9bd632fe4e5e2edb62916" + integrity sha512-i5d+Wh8js4qEWoAsPY5L7HVSWpumr1OhfF2dUCGYdyW6AMqVJPca6+n6zp1Rg2CO+J9norp44XAVVCbyhtUpig== dependencies: - "@backstage/config" "^0.1.3" - "@backstage/core-api" "^0.2.11" - "@backstage/theme" "^0.2.3" + "@backstage/config" "^0.1.1" + "@backstage/core-api" "^0.2.1" + "@backstage/theme" "^0.2.1" + "@material-ui/core" "^4.11.0" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.45" + "@types/dagre" "^0.7.44" + "@types/react" "^16.9" + "@types/react-sparklines" "^1.7.0" + classnames "^2.2.6" + clsx "^1.1.0" + d3-selection "^2.0.0" + d3-shape "^2.0.0" + d3-zoom "^2.0.0" + dagre "^0.8.5" + immer "^7.0.9" + lodash "^4.17.15" + material-table "^1.69.1" + prop-types "^15.7.2" + qs "^6.9.4" + rc-progress "^3.0.0" + react "^16.12.0" + react-dom "^16.12.0" + react-helmet "6.1.0" + react-hook-form "^6.6.0" + react-markdown "^5.0.2" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-sparklines "^1.7.0" + react-syntax-highlighter "^13.5.1" + react-use "^15.3.3" + remark-gfm "^1.0.0" + zen-observable "^0.8.15" + +"@backstage/core@^0.5.0": + version "0.5.0" + resolved "https://registry.npmjs.org/@backstage/core/-/core-0.5.0.tgz#6ff384adc595c18c7db60b9b2d23ebbb9086ed36" + integrity sha512-lCxgKBavUlLYZjZmRF8A7koP4NUhK/tbdf9SaEod0miZg6JTaDoAm3dmHPyqrMBHgoRRCDTxRIxNhj/8vY87oA== + dependencies: + "@backstage/config" "^0.1.2" + "@backstage/core-api" "^0.2.8" + "@backstage/theme" "^0.2.2" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" @@ -1877,20 +1920,53 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" -"@backstage/plugin-catalog@^0.2.1": - version "0.4.0" +"@backstage/plugin-catalog-react@^0.0.2": + version "0.0.2" + resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.0.2.tgz#e50da2dac9fab3a0d5973f8d1083ee2c368e5e52" + integrity sha512-O6aujFPRaEFTk4XlwOoswbnoHIOqMtj6ycUj6R1mNKOM4plUgGDKKhO3be69FHMJEMbiSvVe6AW+1kXaK+1LqA== + dependencies: + "@backstage/catalog-client" "^0.3.5" + "@backstage/catalog-model" "^0.7.1" + "@backstage/core" "^0.6.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" + +"@backstage/plugin-catalog-react@^0.0.4": + version "0.0.4" + resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.0.4.tgz#a4c8ba90cf48106ac6af2e03afa6338010a1299b" + integrity sha512-1fAqULJvLyE+3SeZ2yxDJnJ3SbUFv2Im55d3KbMgRaSog1chSJJoO3jbIwIRQIBXjRmCXrZbf56qwwWwxj6OjA== dependencies: "@backstage/catalog-client" "^0.3.6" - "@backstage/catalog-model" "^0.7.2" - "@backstage/core" "^0.6.3" - "@backstage/plugin-catalog-react" "^0.1.0" - "@backstage/theme" "^0.2.3" + "@backstage/catalog-model" "^0.7.1" + "@backstage/core" "^0.6.2" + "@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" + +"@backstage/plugin-catalog@^0.2.1": + version "0.2.14" + resolved "https://registry.npmjs.org/@backstage/plugin-catalog/-/plugin-catalog-0.2.14.tgz#50a4176a55ffa543a426ec78cbc9deaecdbcf2b7" + integrity sha512-lDmNcC+m1zbbzYATUp5yIZ5PUp+YyBc1KKu3CCgqjLWSbJ1aJrU1N4g59euel1l2+qSW+lH76Kkp6ZYpZbSO9A== + dependencies: + "@backstage/catalog-client" "^0.3.5" + "@backstage/catalog-model" "^0.7.0" + "@backstage/core" "^0.5.0" + "@backstage/plugin-scaffolder" "^0.4.1" + "@backstage/theme" "^0.2.2" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" "@types/react" "^16.9" classnames "^2.2.6" git-url-parse "^11.4.4" + moment "^2.26.0" react "^16.13.1" react-dom "^16.13.1" react-helmet "6.1.0" @@ -1900,12 +1976,15 @@ swr "^0.3.0" "@backstage/plugin-catalog@^0.3.1": - version "0.4.0" + version "0.3.2" + resolved "https://registry.npmjs.org/@backstage/plugin-catalog/-/plugin-catalog-0.3.2.tgz#06945f10fd678efdade3f2795590c12433568fa0" + integrity sha512-iHLxPHRN9nYIXwOEAQ06m+PagsFb6Nb/XjJSebCAnSrAxPPITvBCfxc2H1GbyyMdC7KAr1ozORB/FFkNsCaQJg== dependencies: "@backstage/catalog-client" "^0.3.6" - "@backstage/catalog-model" "^0.7.2" - "@backstage/core" "^0.6.3" - "@backstage/plugin-catalog-react" "^0.1.0" + "@backstage/catalog-model" "^0.7.1" + "@backstage/core" "^0.6.2" + "@backstage/plugin-catalog-react" "^0.0.4" + "@backstage/plugin-scaffolder" "^0.5.1" "@backstage/theme" "^0.2.3" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" @@ -1946,6 +2025,56 @@ react-use "^15.3.3" swr "^0.3.0" +"@backstage/plugin-scaffolder@^0.4.1": + version "0.4.2" + resolved "https://registry.npmjs.org/@backstage/plugin-scaffolder/-/plugin-scaffolder-0.4.2.tgz#58159227997f7e248ce52535bc32f19fcd0990dc" + integrity sha512-YuyHM587Rqg6KufxfFqQdI7dsZniBM/11Aj8Q0m5ZszOpCuNmDDkR1VX8MKHTBJ709mnLAqRgArdla7FOrOAXQ== + dependencies: + "@backstage/catalog-model" "^0.7.1" + "@backstage/core" "^0.6.0" + "@backstage/plugin-catalog-react" "^0.0.2" + "@backstage/theme" "^0.2.3" + "@material-ui/core" "^4.11.0" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.45" + "@rjsf/core" "^2.4.0" + "@rjsf/material-ui" "^2.4.0" + classnames "^2.2.6" + git-url-parse "^11.4.4" + moment "^2.26.0" + react "^16.13.1" + react-dom "^16.13.1" + react-lazylog "^4.5.2" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-use "^15.3.3" + swr "^0.3.0" + +"@backstage/plugin-scaffolder@^0.5.1": + version "0.5.1" + resolved "https://registry.npmjs.org/@backstage/plugin-scaffolder/-/plugin-scaffolder-0.5.1.tgz#9d36f6b01991ddd9f9f2996068f3f31c766db8db" + integrity sha512-EG+iUc107bneVBPQpFKGp2jD9Y9+x50g/gY6TBN1je8TkgyluoxMj7wKv9e+d4TeGzXwK/LW/suajG9Zo0TJGQ== + dependencies: + "@backstage/catalog-model" "^0.7.1" + "@backstage/core" "^0.6.2" + "@backstage/plugin-catalog-react" "^0.0.4" + "@backstage/theme" "^0.2.3" + "@material-ui/core" "^4.11.0" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.45" + "@rjsf/core" "^2.4.0" + "@rjsf/material-ui" "^2.4.0" + classnames "^2.2.6" + git-url-parse "^11.4.4" + moment "^2.26.0" + react "^16.13.1" + react-dom "^16.13.1" + react-lazylog "^4.5.2" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-use "^15.3.3" + swr "^0.3.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -15076,6 +15205,11 @@ immer@1.10.0: resolved "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== +immer@^7.0.9: + version "7.0.15" + resolved "https://registry.npmjs.org/immer/-/immer-7.0.15.tgz#dc3bc6db87401659d2e737c67a21b227c484a4ad" + integrity sha512-yM7jo9+hvYgvdCQdqvhCNRRio0SCXc8xDPzA25SvKWa7b1WVPjLwQs1VYU5JPXjcJPTqAa5NP5dqpORGYBQ2AA== + immer@^8.0.1: version "8.0.1" resolved "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" @@ -18630,7 +18764,7 @@ modify-values@^1.0.0: resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -moment@^2.19.3, moment@^2.25.3, moment@^2.27.0: +moment@^2.19.3, moment@^2.25.3, moment@^2.26.0, moment@^2.27.0: version "2.29.1" resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== @@ -24025,6 +24159,11 @@ stream-http@^2.7.2: to-arraybuffer "^1.0.0" xtend "^4.0.0" +stream-mock@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/stream-mock/-/stream-mock-2.0.5.tgz#c99d24bd6dbb0eaa57cf6ffefdb064150747826e" + integrity sha512-dx9skT8QYjwLsal+MhGHr4UtgS49brw851C/oTixmhCi4Ip+/qnZmhV1qOcznYYAED6gYKmKea+jjza4/wjpSg== + stream-shift@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" From cebda81000ec35d680d1c6d0a8f98eb6e4ff9a8c Mon Sep 17 00:00:00 2001 From: erdoganoksuz Date: Thu, 25 Feb 2021 17:24:49 +0300 Subject: [PATCH 029/147] documentation added MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can Bilgiç --- docs/features/techdocs/using-cloud-storage.md | 91 +++++++++ .../techdocs-common/__mocks__/pkgcloud.ts | 44 ++++- packages/techdocs-common/package.json | 3 +- .../src/stages/publish/openStackSwift.test.ts | 14 +- .../src/stages/publish/openStackSwift.ts | 24 +-- plugins/techdocs-backend/package.json | 2 +- yarn.lock | 184 +++--------------- 7 files changed, 171 insertions(+), 191 deletions(-) diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index cd4b058644..66e6e15811 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -309,3 +309,94 @@ 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. + +## Configuring OpenStack Swift Container with TechDocs + +Follow the +[official OpenStack Api documentation](https://docs.openstack.org/api-ref/identity/v3/) +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 `'openStackSwift'`. + +```yaml +techdocs: + publisher: + type: 'openStackSwift' +``` + +**2. Create an Azure Blob Storage Container** + +Create a dedicated container for TechDocs sites. +[Refer to the official documentation](https://docs.openstack.org/mitaka/user-guide/dashboard_manage_containers.html). + +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. + +Set the config `techdocs.publisher.openStackSwift.containerName` in your +`app-config.yaml` to the name of the container you just created. + +```yaml +techdocs: + publisher: + type: 'openStackSwift' + openStackSwift: + containerName: 'name-of-techdocs-storage-container' +``` + +**3a. (Recommended) Authentication using environment variable** + +Set the config `techdocs.publisher.openStackSwift.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. +[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 +techdocs: + publisher: + type: 'azureBlobStorage' + azureBlobStorage: + containerName: 'name-of-techdocs-storage-bucket' + credentials: + 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/en-us/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 +``` + +**4. That's it!** + +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. diff --git a/packages/techdocs-common/__mocks__/pkgcloud.ts b/packages/techdocs-common/__mocks__/pkgcloud.ts index bf729a7bd2..4316630928 100644 --- a/packages/techdocs-common/__mocks__/pkgcloud.ts +++ b/packages/techdocs-common/__mocks__/pkgcloud.ts @@ -16,7 +16,7 @@ import fs from 'fs-extra'; import os from 'os'; import path from 'path'; -import { ObjectWritableMock, BufferReadableMock } from 'stream-mock'; +import { EventEmitter } from 'events'; const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; @@ -61,16 +61,44 @@ class PkgCloudStorageClient { } } - upload() { - return new ObjectWritableMock(); + upload({ remote }: { remote: string }) { + const filePath = path.join(rootDir, remote); + + const emitter = new EventEmitter(); + + process.nextTick(() => { + if (fs.existsSync(filePath)) { + emitter.emit('success'); + (emitter as any).end = () => true; + } else { + emitter.emit( + 'error', + new Error(`The file ${filePath} does not exist !`), + ); + } + }); + + return emitter; } - download() { - const stringify = JSON.stringify({ - "site_description": 'site_content', - "site_name": "backstage" + download({ remote }: { remote: string }) { + const filePath = path.join(rootDir, remote); + + const emitter = new EventEmitter(); + + process.nextTick(() => { + if (fs.existsSync(filePath)) { + emitter.emit('data', Buffer.from(fs.readFileSync(filePath))); + emitter.emit('end'); + } else { + emitter.emit( + 'error', + new Error(`The file ${filePath} does not exist !`), + ); + } }); - return new BufferReadableMock([stringify]); + + return emitter; } } diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 2598de1dce..62d7b73f73 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-common", "description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "0.4.2", + "version": "0.5.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -58,7 +58,6 @@ "p-limit": "^3.1.0", "pkgcloud": "^2.2.0", "recursive-readdir": "^2.2.2", - "stream-mock": "^2.0.5", "winston": "^3.2.1" }, "devDependencies": { diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts index fab0bf3a12..30ec8a59b9 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts @@ -111,14 +111,12 @@ describe('OpenStackSwiftPublish', () => { const entity = createMockEntity(); const entityRootDir = getEntityRootDir(entity); - setTimeout(async () => { - expect( - await publisher.publish({ - entity, - directory: entityRootDir, - }), - ).toBeUndefined() - }, 5000); + expect( + await publisher.publish({ + entity, + directory: entityRootDir, + }), + ).toBeUndefined() }); it('should fail to publish a directory', async () => { diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.ts index 60c8349dee..4c4c15d9c4 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.ts +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.ts @@ -49,7 +49,7 @@ export class OpenStackSwiftPublish implements PublisherBase { } catch (error) { throw new Error( "Since techdocs.publisher.type is set to 'openStackSwift' in your app config, " + - 'techdocs.publisher.openStackSwift.containerName is required.', + 'techdocs.publisher.openStackSwift.containerName is required.', ); } @@ -59,8 +59,8 @@ export class OpenStackSwiftPublish implements PublisherBase { const storageClient = storage.createClient({ provider: 'openstack', - username: openStackSwiftConfig.getString('username'), - password: openStackSwiftConfig.getString('password'), + username: openStackSwiftConfig.getString('credentials.username'), + password: openStackSwiftConfig.getString('credentials.password'), authUrl: openStackSwiftConfig.getString('authUrl'), keystoneAuthVersion: openStackSwiftConfig.getOptionalString('keystoneAuthVersion') || 'v3', @@ -80,9 +80,9 @@ export class OpenStackSwiftPublish implements PublisherBase { } else { logger.error( `Could not retrieve metadata about the OpenStack Swift container ${containerName}. ` + - 'Make sure the container exists. Also make sure that authentication is setup either by ' + - 'explicitly defining credentials and region in techdocs.publisher.openStackSwift in app config or ' + - 'by using environment variables. Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage', + 'Make sure the container exists. Also make sure that authentication is setup either by ' + + 'explicitly defining credentials and region in techdocs.publisher.openStackSwift in app config or ' + + 'by using environment variables. Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage', ); logger.error(`from OpenStack client library: ${err.message}`); @@ -139,15 +139,15 @@ export class OpenStackSwiftPublish implements PublisherBase { // Rate limit the concurrent execution of file uploads to batches of 10 (per publish) const uploadFile = limiter(() => - new Promise((res, rej) => { - const writeStream = this.storageClient.upload(params); + new Promise((res, rej) => { + const writeStream = this.storageClient.upload(params); - writeStream.on('error', rej); + writeStream.on('error', rej); - writeStream.on('success', res); + writeStream.on('success', res); - readStream.pipe(writeStream); - }), + readStream.pipe(writeStream); + }), ); uploadPromises.push(uploadFile); } diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 03bea5233d..0de3393097 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "0.6.2", + "version": "0.7.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/yarn.lock b/yarn.lock index 5b7893c852..0f2cbbb7d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1816,85 +1816,44 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.2.0": - version "0.2.0" - resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.2.0.tgz#e3fe2a4ddeb6a9b6ec480c80cb2b9c39cb245576" - integrity sha512-Y1ocdRpBlxK/VrJQjHlQd0bgADECd1B2NRjwd8ss46ibT5hwLvMOfD80+Fa7oPLu0ktJrH4lq0pNIIJIml48zA== + version "0.7.2" dependencies: - "@backstage/config" "^0.1.1" + "@backstage/config" "^0.1.3" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" + ajv "^7.0.3" json-schema "^0.2.5" lodash "^4.17.15" uuid "^8.0.0" yup "^0.29.3" "@backstage/catalog-model@^0.3.0": - version "0.3.1" - resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.3.1.tgz#45d08e2f333c9c566b2bf2629fd707fe989bb404" - integrity sha512-9XhV7c4rmVW+Yzj2PiwTQ7DsegWGB3C4ELsDRExuEVZONdqNcC02cyJtrt3fT5F31ZS3tHkB9bMUymFOBLqUSA== + version "0.7.2" dependencies: - "@backstage/config" "^0.1.1" + "@backstage/config" "^0.1.3" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" + ajv "^7.0.3" json-schema "^0.2.5" lodash "^4.17.15" uuid "^8.0.0" yup "^0.29.3" "@backstage/core@^0.3.0": - version "0.3.2" - resolved "https://registry.npmjs.org/@backstage/core/-/core-0.3.2.tgz#a8209126d5076cf4a8b9bd632fe4e5e2edb62916" - integrity sha512-i5d+Wh8js4qEWoAsPY5L7HVSWpumr1OhfF2dUCGYdyW6AMqVJPca6+n6zp1Rg2CO+J9norp44XAVVCbyhtUpig== + version "0.6.3" dependencies: - "@backstage/config" "^0.1.1" - "@backstage/core-api" "^0.2.1" - "@backstage/theme" "^0.2.1" - "@material-ui/core" "^4.11.0" - "@material-ui/icons" "^4.9.1" - "@material-ui/lab" "4.0.0-alpha.45" - "@types/dagre" "^0.7.44" - "@types/react" "^16.9" - "@types/react-sparklines" "^1.7.0" - classnames "^2.2.6" - clsx "^1.1.0" - d3-selection "^2.0.0" - d3-shape "^2.0.0" - d3-zoom "^2.0.0" - dagre "^0.8.5" - immer "^7.0.9" - lodash "^4.17.15" - material-table "^1.69.1" - prop-types "^15.7.2" - qs "^6.9.4" - rc-progress "^3.0.0" - react "^16.12.0" - react-dom "^16.12.0" - react-helmet "6.1.0" - react-hook-form "^6.6.0" - react-markdown "^5.0.2" - react-router "6.0.0-beta.0" - react-router-dom "6.0.0-beta.0" - react-sparklines "^1.7.0" - react-syntax-highlighter "^13.5.1" - react-use "^15.3.3" - remark-gfm "^1.0.0" - zen-observable "^0.8.15" - -"@backstage/core@^0.5.0": - version "0.5.0" - resolved "https://registry.npmjs.org/@backstage/core/-/core-0.5.0.tgz#6ff384adc595c18c7db60b9b2d23ebbb9086ed36" - integrity sha512-lCxgKBavUlLYZjZmRF8A7koP4NUhK/tbdf9SaEod0miZg6JTaDoAm3dmHPyqrMBHgoRRCDTxRIxNhj/8vY87oA== - dependencies: - "@backstage/config" "^0.1.2" - "@backstage/core-api" "^0.2.8" - "@backstage/theme" "^0.2.2" + "@backstage/config" "^0.1.3" + "@backstage/core-api" "^0.2.11" + "@backstage/theme" "^0.2.3" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" + "@testing-library/react-hooks" "^3.4.2" "@types/dagre" "^0.7.44" "@types/prop-types" "^15.7.3" "@types/react" "^16.9" "@types/react-sparklines" "^1.7.0" + "@types/react-text-truncate" "^0.14.0" classnames "^2.2.6" clsx "^1.1.0" d3-selection "^2.0.0" @@ -1916,57 +1875,25 @@ react-router-dom "6.0.0-beta.0" react-sparklines "^1.7.0" react-syntax-highlighter "^13.5.1" + react-text-truncate "^0.16.0" react-use "^15.3.3" remark-gfm "^1.0.0" zen-observable "^0.8.15" -"@backstage/plugin-catalog-react@^0.0.2": - version "0.0.2" - resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.0.2.tgz#e50da2dac9fab3a0d5973f8d1083ee2c368e5e52" - integrity sha512-O6aujFPRaEFTk4XlwOoswbnoHIOqMtj6ycUj6R1mNKOM4plUgGDKKhO3be69FHMJEMbiSvVe6AW+1kXaK+1LqA== - dependencies: - "@backstage/catalog-client" "^0.3.5" - "@backstage/catalog-model" "^0.7.1" - "@backstage/core" "^0.6.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" - -"@backstage/plugin-catalog-react@^0.0.4": - version "0.0.4" - resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.0.4.tgz#a4c8ba90cf48106ac6af2e03afa6338010a1299b" - integrity sha512-1fAqULJvLyE+3SeZ2yxDJnJ3SbUFv2Im55d3KbMgRaSog1chSJJoO3jbIwIRQIBXjRmCXrZbf56qwwWwxj6OjA== +"@backstage/plugin-catalog@^0.2.1": + version "0.4.0" dependencies: "@backstage/catalog-client" "^0.3.6" - "@backstage/catalog-model" "^0.7.1" - "@backstage/core" "^0.6.2" - "@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" - -"@backstage/plugin-catalog@^0.2.1": - version "0.2.14" - resolved "https://registry.npmjs.org/@backstage/plugin-catalog/-/plugin-catalog-0.2.14.tgz#50a4176a55ffa543a426ec78cbc9deaecdbcf2b7" - integrity sha512-lDmNcC+m1zbbzYATUp5yIZ5PUp+YyBc1KKu3CCgqjLWSbJ1aJrU1N4g59euel1l2+qSW+lH76Kkp6ZYpZbSO9A== - dependencies: - "@backstage/catalog-client" "^0.3.5" - "@backstage/catalog-model" "^0.7.0" - "@backstage/core" "^0.5.0" - "@backstage/plugin-scaffolder" "^0.4.1" - "@backstage/theme" "^0.2.2" + "@backstage/catalog-model" "^0.7.2" + "@backstage/core" "^0.6.3" + "@backstage/plugin-catalog-react" "^0.1.0" + "@backstage/theme" "^0.2.3" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" "@types/react" "^16.9" classnames "^2.2.6" git-url-parse "^11.4.4" - moment "^2.26.0" react "^16.13.1" react-dom "^16.13.1" react-helmet "6.1.0" @@ -1976,15 +1903,12 @@ swr "^0.3.0" "@backstage/plugin-catalog@^0.3.1": - version "0.3.2" - resolved "https://registry.npmjs.org/@backstage/plugin-catalog/-/plugin-catalog-0.3.2.tgz#06945f10fd678efdade3f2795590c12433568fa0" - integrity sha512-iHLxPHRN9nYIXwOEAQ06m+PagsFb6Nb/XjJSebCAnSrAxPPITvBCfxc2H1GbyyMdC7KAr1ozORB/FFkNsCaQJg== + version "0.4.0" dependencies: "@backstage/catalog-client" "^0.3.6" - "@backstage/catalog-model" "^0.7.1" - "@backstage/core" "^0.6.2" - "@backstage/plugin-catalog-react" "^0.0.4" - "@backstage/plugin-scaffolder" "^0.5.1" + "@backstage/catalog-model" "^0.7.2" + "@backstage/core" "^0.6.3" + "@backstage/plugin-catalog-react" "^0.1.0" "@backstage/theme" "^0.2.3" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" @@ -2025,56 +1949,6 @@ react-use "^15.3.3" swr "^0.3.0" -"@backstage/plugin-scaffolder@^0.4.1": - version "0.4.2" - resolved "https://registry.npmjs.org/@backstage/plugin-scaffolder/-/plugin-scaffolder-0.4.2.tgz#58159227997f7e248ce52535bc32f19fcd0990dc" - integrity sha512-YuyHM587Rqg6KufxfFqQdI7dsZniBM/11Aj8Q0m5ZszOpCuNmDDkR1VX8MKHTBJ709mnLAqRgArdla7FOrOAXQ== - dependencies: - "@backstage/catalog-model" "^0.7.1" - "@backstage/core" "^0.6.0" - "@backstage/plugin-catalog-react" "^0.0.2" - "@backstage/theme" "^0.2.3" - "@material-ui/core" "^4.11.0" - "@material-ui/icons" "^4.9.1" - "@material-ui/lab" "4.0.0-alpha.45" - "@rjsf/core" "^2.4.0" - "@rjsf/material-ui" "^2.4.0" - classnames "^2.2.6" - git-url-parse "^11.4.4" - moment "^2.26.0" - react "^16.13.1" - react-dom "^16.13.1" - react-lazylog "^4.5.2" - react-router "6.0.0-beta.0" - react-router-dom "6.0.0-beta.0" - react-use "^15.3.3" - swr "^0.3.0" - -"@backstage/plugin-scaffolder@^0.5.1": - version "0.5.1" - resolved "https://registry.npmjs.org/@backstage/plugin-scaffolder/-/plugin-scaffolder-0.5.1.tgz#9d36f6b01991ddd9f9f2996068f3f31c766db8db" - integrity sha512-EG+iUc107bneVBPQpFKGp2jD9Y9+x50g/gY6TBN1je8TkgyluoxMj7wKv9e+d4TeGzXwK/LW/suajG9Zo0TJGQ== - dependencies: - "@backstage/catalog-model" "^0.7.1" - "@backstage/core" "^0.6.2" - "@backstage/plugin-catalog-react" "^0.0.4" - "@backstage/theme" "^0.2.3" - "@material-ui/core" "^4.11.0" - "@material-ui/icons" "^4.9.1" - "@material-ui/lab" "4.0.0-alpha.45" - "@rjsf/core" "^2.4.0" - "@rjsf/material-ui" "^2.4.0" - classnames "^2.2.6" - git-url-parse "^11.4.4" - moment "^2.26.0" - react "^16.13.1" - react-dom "^16.13.1" - react-lazylog "^4.5.2" - react-router "6.0.0-beta.0" - react-router-dom "6.0.0-beta.0" - react-use "^15.3.3" - swr "^0.3.0" - "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -15205,11 +15079,6 @@ immer@1.10.0: resolved "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== -immer@^7.0.9: - version "7.0.15" - resolved "https://registry.npmjs.org/immer/-/immer-7.0.15.tgz#dc3bc6db87401659d2e737c67a21b227c484a4ad" - integrity sha512-yM7jo9+hvYgvdCQdqvhCNRRio0SCXc8xDPzA25SvKWa7b1WVPjLwQs1VYU5JPXjcJPTqAa5NP5dqpORGYBQ2AA== - immer@^8.0.1: version "8.0.1" resolved "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" @@ -18764,7 +18633,7 @@ modify-values@^1.0.0: resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -moment@^2.19.3, moment@^2.25.3, moment@^2.26.0, moment@^2.27.0: +moment@^2.19.3, moment@^2.25.3, moment@^2.27.0: version "2.29.1" resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== @@ -24159,11 +24028,6 @@ stream-http@^2.7.2: to-arraybuffer "^1.0.0" xtend "^4.0.0" -stream-mock@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/stream-mock/-/stream-mock-2.0.5.tgz#c99d24bd6dbb0eaa57cf6ffefdb064150747826e" - integrity sha512-dx9skT8QYjwLsal+MhGHr4UtgS49brw851C/oTixmhCi4Ip+/qnZmhV1qOcznYYAED6gYKmKea+jjza4/wjpSg== - stream-shift@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" From fc470a310ea2d439ea95d8cdbe2c9f5e92a29139 Mon Sep 17 00:00:00 2001 From: gmzsenturk Date: Thu, 25 Feb 2021 17:50:20 +0300 Subject: [PATCH 030/147] Updated cloud storage documentation for open stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can Bilgiç --- docs/features/techdocs/using-cloud-storage.md | 69 +++++++------------ 1 file changed, 25 insertions(+), 44 deletions(-) diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index 66e6e15811..e160c6f5f5 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -281,11 +281,9 @@ techdocs: **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. +Set the config `techdocs.publisher.azureBlobStorage.credentials.accountName` in +your `app-config.yaml` to the your account name. -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/en-us/rest/api/storageservices/authorize-with-shared-key for more details. @@ -314,7 +312,7 @@ the logs. Follow the [official OpenStack Api documentation](https://docs.openstack.org/api-ref/identity/v3/) -for the latest instructions on the following steps involving Azure Blob Storage. +for the latest instructions on the following steps involving Open Stack Storage. **1. Set `techdocs.publisher.type` config in your `app-config.yaml`** @@ -326,7 +324,7 @@ techdocs: type: 'openStackSwift' ``` -**2. Create an Azure Blob Storage Container** +**2. Create an OpenStack Swift Storage Container** Create a dedicated container for TechDocs sites. [Refer to the official documentation](https://docs.openstack.org/mitaka/user-guide/dashboard_manage_containers.html). @@ -346,57 +344,40 @@ techdocs: containerName: 'name-of-techdocs-storage-container' ``` -**3a. (Recommended) Authentication using environment variable** +**3. Authentication using app-config.yaml** -Set the config `techdocs.publisher.openStackSwift.accountName` in -your `app-config.yaml` to the your account name. +Set the configs in your `app-config.yaml` to the your container 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. -[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 -techdocs: - publisher: - type: 'azureBlobStorage' - azureBlobStorage: - containerName: 'name-of-techdocs-storage-bucket' - credentials: - 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/en-us/rest/api/storageservices/authorize-with-shared-key +https://docs.openstack.org/api-ref/identity/v3/?expanded=password-authentication-with-unscoped-authorization-detail#password-authentication-with-unscoped-authorization for more details. ```yaml techdocs: publisher: - type: 'azureBlobStorage' - azureBlobStorage: + type: 'openStackSwift' + openStackSwift: containerName: 'name-of-techdocs-storage-bucket' credentials: - accountName: - $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_NAME - accountKey: - $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_KEY + userName: + $env: OPENSTACK_SWIFT_STORAGE_USERNAME + password: + $env: OPENSTACK_SWIFT_STORAGE_PASSWORD + authUrl: + $env: OPENSTACK_SWIFT_STORAGE_AUTH_URL + keystoneAuthVersion: + $env: OPENSTACK_SWIFT_STORAGE_AUTH_VERSION + domainId: + $env: OPENSTACK_SWIFT_STORAGE_DOMAIN_ID + domainName: + $env: OPENSTACK_SWIFT_STORAGE_DOMAIN_NAME + region: + $env: OPENSTACK_SWIFT_STORAGE_REGION ``` **4. That's it!** -Your Backstage app is now ready to use Azure Blob Storage for TechDocs, to store +Your Backstage app is now ready to use OpenStack Swift 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 +`techdocs info Successfully connected to the OpenStack Swift Storage container` in the logs. From 54e711826fb441ee1a7b861f1fec4371e2640562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Can=20Bilgi=C3=A7?= Date: Thu, 25 Feb 2021 17:54:55 +0300 Subject: [PATCH 031/147] capp config type has changed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can Bilgiç --- app-config.yaml | 2 +- .../src/stages/publish/openStackSwift.test.ts | 8 ++-- .../src/stages/publish/publish.test.ts | 6 ++- plugins/techdocs/config.d.ts | 41 ++++++++++--------- 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index f68f5a30a2..be0f405de5 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -94,7 +94,7 @@ techdocs: generators: techdocs: 'docker' # Alternatives - 'local' publisher: - type: 'local' # Alternatives - 'googleGcs' or 'awsS3' or 'azureBlobStorage'. Read documentation for using alternatives. + type: 'local' # Alternatives - 'googleGcs' or 'awsS3' or 'azureBlobStorage' or 'openStackSwift' Read documentation for using alternatives. sentry: organization: my-company diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts index 30ec8a59b9..b831012acf 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts @@ -73,8 +73,10 @@ beforeEach(() => { publisher: { type: 'openStackSwift', openStackSwift: { - username: 'mockuser', - password: 'verystrongpass', + credentials: { + username: 'mockuser', + password: 'verystrongpass', + }, authUrl: 'mockauthurl', region: 'mockregion', containerName: 'mock', @@ -116,7 +118,7 @@ describe('OpenStackSwiftPublish', () => { entity, directory: entityRootDir, }), - ).toBeUndefined() + ).toBeUndefined(); }); it('should fail to publish a directory', async () => { diff --git a/packages/techdocs-common/src/stages/publish/publish.test.ts b/packages/techdocs-common/src/stages/publish/publish.test.ts index f9bd0f3934..948b92aaa6 100644 --- a/packages/techdocs-common/src/stages/publish/publish.test.ts +++ b/packages/techdocs-common/src/stages/publish/publish.test.ts @@ -170,8 +170,10 @@ describe('Publisher', () => { publisher: { type: 'openStackSwift', openStackSwift: { - username: 'mockuser', - password: 'verystrongpass', + credentials: { + username: 'mockuser', + password: 'verystrongpass', + }, authUrl: 'mockauthurl', region: 'mockregion', containerName: 'mock', diff --git a/plugins/techdocs/config.d.ts b/plugins/techdocs/config.d.ts index c4fe4a47db..5e12f7dbc7 100644 --- a/plugins/techdocs/config.d.ts +++ b/plugins/techdocs/config.d.ts @@ -89,56 +89,57 @@ export interface Config { type: 'openStackSwift'; /** - * Required when 'type' is set to awsS3 + * Required when 'type' is set to openStackSwift */ openStackSwift?: { /** - * (Optional) Credentials used to access a storage bucket. - * If not set, environment variables or aws config file will be used to authenticate. - * @see https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html - * @see https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-shared.html + * (Required) Credentials used to access a storage bucket. + * @see https://docs.openstack.org/api-ref/identity/v3/?expanded=password-authentication-with-unscoped-authorization-detail#password-authentication-with-unscoped-authorization * @visibility secret */ + credentials: { + /** + * (Required) Root user name + * @visibility backend + */ + username: string; + /** + * (Required) Root user password + * @visibility backend + */ + password: string; // required + }; /** * (Required) Cloud Storage Container Name * @visibility backend */ containerName: string; - /** - * (Required) Root user name - * @visibility backend - */ - username: string; - /** - * (Required) Root user password - * @visibility backend - */ - password: string; // required /** * (Required) Auth url sometimes OpenStack uses different port check your OpenStack apis. * @visibility backend */ authUrl: string; /** - * (Required) Auth version + * (Optional) Auth version + * If not set, 'v2.0' will be used. * @visibility backend */ keystoneAuthVersion: string; /** - * (Required) Domaind Id + * (Required) Domain Id * @visibility backend */ domainId: string; /** - * (Required) Domaind Name + * (Required) Domain Name * @visibility backend */ - domainName: 'Default'; + domainName: string; /** * (Required) Region * @visibility backend */ - region: 'earth'; + region: string; }; } | { From e0bccb16a22f327224c910470acceefe6b9746fd Mon Sep 17 00:00:00 2001 From: erdoganoksuz Date: Fri, 26 Feb 2021 10:39:08 +0300 Subject: [PATCH 032/147] versions for changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can Bilgiç --- packages/techdocs-common/package.json | 2 +- plugins/techdocs-backend/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 62d7b73f73..a543addfa9 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-common", "description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "0.5.0", + "version": "0.4.2", "main": "src/index.ts", "types": "src/index.ts", "private": false, diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 0de3393097..03bea5233d 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "0.7.0", + "version": "0.6.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From 15fb8e2176ac26bd9b371857299c2a5d75b9799c Mon Sep 17 00:00:00 2001 From: erdoganoksuz Date: Fri, 26 Feb 2021 10:41:43 +0300 Subject: [PATCH 033/147] changeset added MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can Bilgiç --- .changeset/odd-mirrors-smell.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/odd-mirrors-smell.md diff --git a/.changeset/odd-mirrors-smell.md b/.changeset/odd-mirrors-smell.md new file mode 100644 index 0000000000..7b6e0285ef --- /dev/null +++ b/.changeset/odd-mirrors-smell.md @@ -0,0 +1,7 @@ +--- +'@backstage/techdocs-common': minor +'@backstage/plugin-techdocs': minor +'@backstage/plugin-techdocs-backend': minor +--- + +OpenStack Swift Publisher added to TechDocs From 2ebfe29d8a04faa06cb95f312bbce1d9daa6825f Mon Sep 17 00:00:00 2001 From: gmzsenturk Date: Fri, 26 Feb 2021 10:55:16 +0300 Subject: [PATCH 034/147] Cloud storage documentation edited MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can Bilgiç --- docs/features/techdocs/using-cloud-storage.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index e160c6f5f5..6fefca70c0 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -281,8 +281,11 @@ techdocs: **3b. Authentication using app-config.yaml** -Set the config `techdocs.publisher.azureBlobStorage.credentials.accountName` in -your `app-config.yaml` to the your account name. +If you do not prefer (3a) and optionally like to use a service account, you can Set the config `techdocs.publisher.azureBlobStorage.credentials.accountName` in +follow these steps. your `app-config.yaml` to the your account name. + +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/en-us/rest/api/storageservices/authorize-with-shared-key for more details. From a10174b56d82a0552a809bccab4512ec293b01cb Mon Sep 17 00:00:00 2001 From: gmzsenturk Date: Fri, 26 Feb 2021 11:18:41 +0300 Subject: [PATCH 035/147] Documentations edited MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can Bilgiç --- app-config.yaml | 2 +- docs/features/techdocs/using-cloud-storage.md | 17 +++++++------- .../techdocs-common/__mocks__/pkgcloud.ts | 2 +- .../src/stages/publish/openStackSwift.ts | 23 ++++++++++--------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index be0f405de5..f963296bb8 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -94,7 +94,7 @@ techdocs: generators: techdocs: 'docker' # Alternatives - 'local' publisher: - type: 'local' # Alternatives - 'googleGcs' or 'awsS3' or 'azureBlobStorage' or 'openStackSwift' Read documentation for using alternatives. + type: 'local' # Alternatives - 'googleGcs' or 'awsS3' or 'azureBlobStorage' or 'openStackSwift'. Read documentation for using alternatives. sentry: organization: my-company diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index 6fefca70c0..8b8ff04573 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -281,12 +281,11 @@ techdocs: **3b. Authentication using app-config.yaml** -If you do not prefer (3a) and optionally like to use a service account, you can Set the config `techdocs.publisher.azureBlobStorage.credentials.accountName` in -follow these steps. your `app-config.yaml` to the your account name. +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/en-us/rest/api/storageservices/authorize-with-shared-key for more details. @@ -373,14 +372,14 @@ techdocs: $env: OPENSTACK_SWIFT_STORAGE_DOMAIN_ID domainName: $env: OPENSTACK_SWIFT_STORAGE_DOMAIN_NAME - region: + region: $env: OPENSTACK_SWIFT_STORAGE_REGION ``` **4. That's it!** -Your Backstage app is now ready to use OpenStack Swift 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 OpenStack Swift Storage container` in -the logs. +Your Backstage app is now ready to use OpenStack Swift 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 OpenStack Swift Storage container` +in the logs. diff --git a/packages/techdocs-common/__mocks__/pkgcloud.ts b/packages/techdocs-common/__mocks__/pkgcloud.ts index 4316630928..6b250092b8 100644 --- a/packages/techdocs-common/__mocks__/pkgcloud.ts +++ b/packages/techdocs-common/__mocks__/pkgcloud.ts @@ -21,7 +21,7 @@ import { EventEmitter } from 'events'; const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; const checkFileExists = async (Key: string): Promise => { - // Key will always have / as file separator irrespective of OS since S3 expects /. + // Key will always have / as file separator irrespective of OS since cloud providers expects /. // Normalize Key to OS specific path before checking if file exists. const filePath = path.join(rootDir, Key); diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.ts index 4c4c15d9c4..3987a36e17 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.ts +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.ts @@ -49,7 +49,7 @@ export class OpenStackSwiftPublish implements PublisherBase { } catch (error) { throw new Error( "Since techdocs.publisher.type is set to 'openStackSwift' in your app config, " + - 'techdocs.publisher.openStackSwift.containerName is required.', + 'techdocs.publisher.openStackSwift.containerName is required.', ); } @@ -80,9 +80,9 @@ export class OpenStackSwiftPublish implements PublisherBase { } else { logger.error( `Could not retrieve metadata about the OpenStack Swift container ${containerName}. ` + - 'Make sure the container exists. Also make sure that authentication is setup either by ' + - 'explicitly defining credentials and region in techdocs.publisher.openStackSwift in app config or ' + - 'by using environment variables. Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage', + 'Make sure the container exists. Also make sure that authentication is setup either by ' + + 'explicitly defining credentials and region in techdocs.publisher.openStackSwift in app config or ' + + 'by using environment variables. Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage', ); logger.error(`from OpenStack client library: ${err.message}`); @@ -138,16 +138,17 @@ export class OpenStackSwiftPublish implements PublisherBase { }; // Rate limit the concurrent execution of file uploads to batches of 10 (per publish) - const uploadFile = limiter(() => - new Promise((res, rej) => { - const writeStream = this.storageClient.upload(params); + const uploadFile = limiter( + () => + new Promise((res, rej) => { + const writeStream = this.storageClient.upload(params); - writeStream.on('error', rej); + writeStream.on('error', rej); - writeStream.on('success', res); + writeStream.on('success', res); - readStream.pipe(writeStream); - }), + readStream.pipe(writeStream); + }), ); uploadPromises.push(uploadFile); } From 37586e0fc9b0e55c692d6d52a29f52afef71117b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Can=20Bilgi=C3=A7?= Date: Fri, 26 Feb 2021 15:26:28 +0300 Subject: [PATCH 036/147] runned prettier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can Bilgiç --- docs/features/techdocs/using-cloud-storage.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index 8b8ff04573..e6dcd077cd 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -257,7 +257,8 @@ techdocs: **3a. (Recommended) Authentication using environment variable** -Set the config `techdocs.publisher.azureBlobStorage.credentials.accountName` in +If you do not prefer (3a) and optionally like to use a service account, you can +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 From aa095e469fa5f054c7c5757053edbb2cdf4364cb Mon Sep 17 00:00:00 2001 From: erdoganoksuz Date: Mon, 1 Mar 2021 11:20:17 +0300 Subject: [PATCH 037/147] pull request feedbacks fix. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can Bilgiç --- .changeset/nine-comics-smash.md | 7 +++++++ .changeset/odd-mirrors-smell.md | 7 ------- docs/features/techdocs/README.md | 1 + .../src/stages/publish/openStackSwift.ts | 11 +++++++---- plugins/techdocs/config.d.ts | 4 ++-- 5 files changed, 17 insertions(+), 13 deletions(-) create mode 100644 .changeset/nine-comics-smash.md delete mode 100644 .changeset/odd-mirrors-smell.md diff --git a/.changeset/nine-comics-smash.md b/.changeset/nine-comics-smash.md new file mode 100644 index 0000000000..147944930d --- /dev/null +++ b/.changeset/nine-comics-smash.md @@ -0,0 +1,7 @@ +--- +'@backstage/techdocs-common': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-techdocs': patch +--- + +OpenStack Swift publisher added for tech-docs. diff --git a/.changeset/odd-mirrors-smell.md b/.changeset/odd-mirrors-smell.md deleted file mode 100644 index 7b6e0285ef..0000000000 --- a/.changeset/odd-mirrors-smell.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/techdocs-common': minor -'@backstage/plugin-techdocs': minor -'@backstage/plugin-techdocs-backend': minor ---- - -OpenStack Swift Publisher added to TechDocs diff --git a/docs/features/techdocs/README.md b/docs/features/techdocs/README.md index 2fa48f57b0..7025364f78 100644 --- a/docs/features/techdocs/README.md +++ b/docs/features/techdocs/README.md @@ -54,6 +54,7 @@ providers are used. | Google Cloud Storage (GCS) | Yes ✅ | | Amazon Web Services (AWS) S3 | Yes ✅ | | Azure Blob Storage | Yes ✅ | +| OpenStack Swift | Yes ✅ | [Reach out to us](#feedback) if you want to request more platforms. diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.ts index 3987a36e17..0e60bc5796 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.ts +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.ts @@ -130,8 +130,6 @@ export class OpenStackSwiftPublish implements PublisherBase { const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; const destination = `${entityRootDir}/${relativeFilePathPosix}`; // Swift container file relative path - const readStream = fs.createReadStream(filePath, 'utf8'); - const params = { container: this.containerName, remote: destination, @@ -141,6 +139,8 @@ export class OpenStackSwiftPublish implements PublisherBase { const uploadFile = limiter( () => new Promise((res, rej) => { + const readStream = fs.createReadStream(filePath, 'utf8'); + const writeStream = this.storageClient.upload(params); writeStream.on('error', rej); @@ -246,10 +246,13 @@ export class OpenStackSwiftPublish implements PublisherBase { this.storageClient.getFile( this.containerName, `${entityRootDir}/index.html`, - (err: any, file: any) => { + (err, file) => { if (!err && file) { res(true); - } else res(false); + } else { + res(false); + this.logger.warn(err.message); + } }, ); }); diff --git a/plugins/techdocs/config.d.ts b/plugins/techdocs/config.d.ts index 5e12f7dbc7..54d2b8fb69 100644 --- a/plugins/techdocs/config.d.ts +++ b/plugins/techdocs/config.d.ts @@ -100,12 +100,12 @@ export interface Config { credentials: { /** * (Required) Root user name - * @visibility backend + * @visibility secret */ username: string; /** * (Required) Root user password - * @visibility backend + * @visibility secret */ password: string; // required }; From 7bf88b4f311e0c6d1dc7ac234a02f77ce056433f Mon Sep 17 00:00:00 2001 From: erdoganoksuz Date: Tue, 2 Mar 2021 11:53:28 +0300 Subject: [PATCH 038/147] error name and documentation consistency. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can Bilgiç --- docs/features/techdocs/using-cloud-storage.md | 4 ++-- packages/techdocs-common/__mocks__/pkgcloud.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index e6dcd077cd..fd8f320859 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -315,7 +315,7 @@ the logs. Follow the [official OpenStack Api documentation](https://docs.openstack.org/api-ref/identity/v3/) -for the latest instructions on the following steps involving Open Stack Storage. +for the latest instructions on the following steps involving OpenStack Storage. **1. Set `techdocs.publisher.type` config in your `app-config.yaml`** @@ -349,7 +349,7 @@ techdocs: **3. Authentication using app-config.yaml** -Set the configs in your `app-config.yaml` to the your container name. +Set the configs in your `app-config.yaml` to point to your container name. https://docs.openstack.org/api-ref/identity/v3/?expanded=password-authentication-with-unscoped-authorization-detail#password-authentication-with-unscoped-authorization for more details. diff --git a/packages/techdocs-common/__mocks__/pkgcloud.ts b/packages/techdocs-common/__mocks__/pkgcloud.ts index 6b250092b8..5d9f81cda0 100644 --- a/packages/techdocs-common/__mocks__/pkgcloud.ts +++ b/packages/techdocs-common/__mocks__/pkgcloud.ts @@ -54,7 +54,7 @@ class PkgCloudStorageClient { callback: (err: string, container: string) => any, ) { if (containerName !== 'mock') { - callback("Container doesn't exist", containerName); + callback('Container does not exist', containerName); throw new Error('Container does not exist'); } else { callback('Container does not exist', 'success'); From 8871e7523bdb2a5b56a6b8a4f43da2be90f6252a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 3 Mar 2021 15:38:59 +0100 Subject: [PATCH 039/147] Create tidy-ads-juggle.md Signed-off-by: Patrik Oldsberg --- .changeset/tidy-ads-juggle.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tidy-ads-juggle.md diff --git a/.changeset/tidy-ads-juggle.md b/.changeset/tidy-ads-juggle.md new file mode 100644 index 0000000000..72daaf2fe9 --- /dev/null +++ b/.changeset/tidy-ads-juggle.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Bump `ts-loader` dependency range from `^7.0.4` to `^8.0.17`. From e40fafd92fbb9ee5243ae5ef0b505d377e5b310d Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Wed, 3 Mar 2021 16:25:59 +0100 Subject: [PATCH 040/147] Reduce the depth of the node tree Signed-off-by: Dominik Henneke --- .../components/SupportButton/SupportButton.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/core/src/components/SupportButton/SupportButton.tsx b/packages/core/src/components/SupportButton/SupportButton.tsx index 09bd27de2f..eea2b000ca 100644 --- a/packages/core/src/components/SupportButton/SupportButton.tsx +++ b/packages/core/src/components/SupportButton/SupportButton.tsx @@ -65,14 +65,14 @@ const SupportListItem = ({ item }: { item: SupportItem }) => { (link => ( - - )) - .reduce((prev, curr, idx) => [prev,
, curr]) - } + secondary={item.links?.reduce( + (prev, link, idx) => [ + ...prev, + idx > 0 &&
, + , + ], + [], + )} /> ); From 415a3a42da5dd060d769b27bc86acdf856db8c82 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 3 Mar 2021 15:58:44 +0100 Subject: [PATCH 041/147] app: fix app test on windows Signed-off-by: Patrik Oldsberg --- .changeset/strange-carrots-fix.md | 25 +++++++++++++++++++ packages/app/src/App.test.tsx | 10 ++++---- .../default-app/packages/app/src/App.test.tsx | 10 ++++---- 3 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 .changeset/strange-carrots-fix.md diff --git a/.changeset/strange-carrots-fix.md b/.changeset/strange-carrots-fix.md new file mode 100644 index 0000000000..41ca0b9bd9 --- /dev/null +++ b/.changeset/strange-carrots-fix.md @@ -0,0 +1,25 @@ +--- +'@backstage/create-app': patch +--- + +Updated the default `App` test to work better on Windows. + +To apply this change to an existing app, replace the `process.env.APP_CONFIG` definition in `packages/app/src/App.test.tsx` with the following: + +```ts +process.env = { + NODE_ENV: 'test', + APP_CONFIG: [ + { + data: { + app: { title: 'Test' }, + backend: { baseUrl: 'http://localhost:7000' }, + techdocs: { + storageUrl: 'http://localhost:7000/api/techdocs/static/docs', + }, + }, + context: 'test', + }, + ] as any, +}; +``` diff --git a/packages/app/src/App.test.tsx b/packages/app/src/App.test.tsx index cf70a7be27..cdb1e8db4a 100644 --- a/packages/app/src/App.test.tsx +++ b/packages/app/src/App.test.tsx @@ -20,9 +20,9 @@ import App from './App'; describe('App', () => { it('should render', async () => { - Object.defineProperty(process.env, 'APP_CONFIG', { - configurable: true, - value: [ + process.env = { + NODE_ENV: 'test', + APP_CONFIG: [ { data: { app: { @@ -39,8 +39,8 @@ describe('App', () => { }, context: 'test', }, - ], - }); + ] as any, + }; const rendered = await renderWithEffects(); expect(rendered.baseElement).toBeInTheDocument(); diff --git a/packages/create-app/templates/default-app/packages/app/src/App.test.tsx b/packages/create-app/templates/default-app/packages/app/src/App.test.tsx index 21a2eaa703..82bc479858 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.test.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.test.tsx @@ -4,9 +4,9 @@ import App from './App'; describe('App', () => { it('should render', async () => { - Object.defineProperty(process.env, 'APP_CONFIG', { - configurable: true, - value: [ + process.env = { + NODE_ENV: 'test', + APP_CONFIG: [ { data: { app: { title: 'Test' }, @@ -17,8 +17,8 @@ describe('App', () => { }, context: 'test', }, - ], - }); + ] as any, + }; const rendered = await renderWithEffects(); expect(rendered.baseElement).toBeInTheDocument(); From fad9354ed5c3922253e2e5c2e5f42fb14f9a9e58 Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Thu, 25 Feb 2021 00:34:04 -0500 Subject: [PATCH 042/147] Add cost explorer findings Signed-off-by: Brenda Sukh --- .../contrib/aws-cost-explorer-api.md | 330 ++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 plugins/cost-insights/contrib/aws-cost-explorer-api.md diff --git a/plugins/cost-insights/contrib/aws-cost-explorer-api.md b/plugins/cost-insights/contrib/aws-cost-explorer-api.md new file mode 100644 index 0000000000..b70ff73ccf --- /dev/null +++ b/plugins/cost-insights/contrib/aws-cost-explorer-api.md @@ -0,0 +1,330 @@ +# Using the AWS Cost Explorer API with Cost Insights + +Cost Insights implementers are currently expected to write their own Cost Insights client to interact with the frontend plugin. We've looked into short-term solutions which we've since punted on, but wanted to share our findings from our exploration of the AWS Cost Explorer API. + +**Note:** Each request using the Cost Explorer API will incur a cost of \$0.01. +` + +## Authentication + +The CostExplorerClient needs to be initialized with access keys tied to an IAM account. You'll need to attach the Cost Explorer policy to the IAM account to be able to make requests to the Cost Explorer API. + +Cost Explorer permission policy: + +```JSON +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ce:*" + ], + "Resource": [ + "*" + ] + } + ] +} + +``` + +## Setup + +1. Install the AWS Cost Explorer SDK. The AWS docs recommend using the SDK over making calls to the API directly as it simplifies authentication and provides direct access to commands. + +```bash +yarn add @aws-sdk/client-cost-explorer +``` + +## Usage of the SDK + +1. Initiate a CostExplorerClient with the appropriate configuration (e.g. credentials, region). + +```ts +const client = new CostExplorerClient({ + region: 'REGION', + credentials: { + accessKeyId: 'ACCESSKEYID', + secretAccessKey: 'SECRETACCESSKEY', + }, +}); +``` + +2. Initiate a command with the relevant input params. The SDK provides a variety of commands, but you can access most cost data using the [GetCostAndUsageCommand](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetCostAndUsage.html) command. You can find more information on the required input params for each command [here](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_Operations_AWS_Cost_Explorer_Service.html). + +```ts +const command = new GetCostAndUsageCommand(params); +``` + +3. Call the `send` operation on the Cost Explorer client with the command object. + +```ts +// async/await. +try { + const data = await client.send(command); + // process data. +} catch (error) { + // error handling. +} finally { + // finally. +} +``` + +## Implementing methods on [CostInsightsApi](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts) + +The Cost Explorer SDK allows for easy access to cost data for easier implementation of CostInsightsApi methods such as `getGroupDailyCost`, `getProjectDailyCost`, and `getProductInsights`. + +### 1. [getGroupDailyCost](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts#L93) + +The `getGroupDailyCost` method is expected to return daily cost aggregations for a given group and interval time frame as a [`Cost`](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/types/Cost.ts). + +#### Total Daily cost + +By using the `GetCostAndUsageCommand` command with some simple parameters, you should be able to use the response to construct a `Cost` object to represent daily cost aggregations. + +Sample command: + +```ts +const command = new GetCostAndUsageCommand({ + TimePeriod: { Start: '2020-12-01', End: '2021-01-02' }, + Metrics: ['Unblended Cost'], + Filter: { + Dimensions: { Key: 'LINKED_ACCOUNT', Values: ['ACCOUNT_ID'] }, + }, + Granularity: 'DAILY', +}); +``` + +The response provides a `ResultsByTime` array, where each [object](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_ResultByTime.html) contains the time period and costs for a single day. This should give you enough data to represent the `aggregation` field with some minor reformatting. + +Sample response: + +```ts + { + ... + ResultsByTime: [ + { + Estimated: false, + TimePeriod: { + End: '2020-12-02', + Start: '2020-12-01', + }, + Total: { UnblendedCost: { Amount: '9392.30', Unit: 'USD' } }, + }, + { + Estimated: false, + TimePeriod: { + End: '2020-12-03', + Start: '2020-12-02', + }, + Total: { UnblendedCost: { Amount: '1543.93', Unit: 'USD' } }, + }, + ], + } +``` + +#### Daily cost breakdown + +You can optionally provide a `groupedCosts` field on the `Cost` object to view daily costs broken down by specific groupings. Each key-value pair on `groupedCosts` will result in an additional tab in the top panel that displays an area chart of costs broken down by that key. + +You can get grouped daily costs from the API by using the `GetCostAndUsageCommand` command with a [`GroupBy`](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetCostAndUsage.html#awscostmanagement-GetCostAndUsage-request-GroupBy) parameter. For example, you can use the `SERVICE` dimension to group costs by cloud product. + +Sample command: + +```ts +new GetCostAndUsageCommand({ + TimePeriod: { Start: '2020-12-01', End: '2021-01-02' }, + Metrics: ['Unblended Cost'], + Filter: { + Dimensions: { Key: 'LINKED_ACCOUNT', Values: ['ACCOUNT_ID'] }, + }, + Granularity: 'DAILY', + GroupBy: [ + { + Type: 'DIMENSION', + Key: 'SERVICE', + }, + ], +}); +``` + +The `ResultsByTime` objects provide a `Groups` array with an entry for each group and its costs for the given day. Since `groupedCosts` expects a separate `Cost` object per group, you'll need to aggregate daily cost data for each group from the response. + +Sample Response: + +```ts +{ + ... + ResultsByTime: [ + { + TimePeriod: { + End: '2020-12-02', + Start: '2020-12-01', + }, + Estimated: false, + Groups: [ + { + Keys: ['Amazon Simple Storage Service'], + Metrics: { + UnblendedCost: { Amount: '169.442', Unit: 'USD' }, + }, + }, + { + Keys: ['Amazon Relational Database Service'], + Metrics: { + UnblendedCost: { Amount: '74.11115', Unit: 'USD' }, + }, + }, + ... + ], + }, + ], +} +``` + +Sample `groupedCosts` based on the response: + +```ts +{ + product: [ + { + id: 'Amazon Simple Storage Service', + aggregation: [ + { date: '2020-12-01', amount: 169.442 }, + ..., + ] + }, + { + id: 'Amazon Relational Database Service', + aggregation: [ + { date: '2020-12-01', amount: 74.11115 }, + ... + ] + }, + ] +} +``` + +### 2. [getProjectDailyCost](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts#L111) + +The `getProjectDailyCost` method is expected to return daily cost aggregations for a given billing entity and interval time frame as a [`Cost`](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/types/Cost.ts). + +This should be similar to the `getGroupDailyCost` method implementation, but with an updated `LINKED_ACCOUNT` filter to get narrower cost data for a lower-level linked account. + +### 3. [getProductInsights](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts#L111) + +The `getProductInsights` method is expected to return cost aggregations for a particular cloud product and interval time frame as an [`Entity`](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/types/Entity.ts). + +#### Cloud product cost by resource + +The Cost Explorer API doesn't provide resource level costs by default. To achieve this, you need to add [cost allocation tags](#cost-allocation-tags) to each resource and use those tags to group costs. + +Once resources have been tagged, we can filter by the service (ex: S3) and group by the tag key (ex: S3-Bucket-Name). Any resources that do not have a value for the given tag will be grouped into a single cost group where the key is not the bucket name, but the tag key with a $ appended (ex: S3-Bucket-Name$). We can aggregate the daily costs for each resource and provide the data for the comparison in the bar chart. + +Sample command: + +```ts +const command = new GetCostAndUsageCommand({ + TimePeriod: { + End: '2020-12-02', + Start: '2020-12-01', + }, + Metrics: ['UnblendedCost'], + Granularity: 'DAILY', + Filter: { + And: [ + { Dimensions: { Key: 'LINKED_ACCOUNT', Values: ['ACCOUNT_ID'] } }, + { + Dimensions: { + Key: 'SERVICE', + Values: ['Amazon Simple Storage Service'], + }, + }, + ], + }, + GroupBy: [{ Type: 'TAG', Key: 'S3-Bucket-Name' }], +}); +``` + +The `ResultsByTime` objects provide a `Groups` array with an entry for each resource and its costs for the given day. You'll need to aggregate cost data into two bucketed time periods (e.g. month vs month, or quarter vs quarter) for each resource since this is the expected data type for the `aggregation` field on `Entity`. + +Sample response: + +```ts +{ + ... + ResultsByTime: [ + { + TimePeriod: { + End: '2020-12-02', + Start: '2020-12-01', + }, + Estimated: false, + Groups: [ + { + Keys: ['S3-Bucket-Name$'], + Metrics: { + UnblendedCost: { Amount: '345.23', Unit: 'USD' }, + }, + }, + { + Keys: ['Bucket1'], + Metrics: { + UnblendedCost: { Amount: '111.09', Unit: 'USD' }, + }, + }, + { + Keys: ['Bucket2'], + Metrics: { + UnblendedCost: { Amount: '254.1111', Unit: 'USD' }, + }, + }, + ], + }, + ], + }; + +``` + +#### Resource cost breakdown + +There are a couple of options for breaking down costs for a resource. + +1. We can use `USAGE_TYPE` (units used to measure usage for each service) supported by the `GroupBy` field. This requires the lowest amount of effort as we can easily add it to the existing resource request. Examples usage types for EC2: BoxUsage:c1.medium(Hrs), BoxUsage:m3.xlarge(Hrs), and BoxUsage:t1.micro(Hrs) + +2. Similar to resources, we can use custom tags to group the data. For example, you’re able to tag objects within a bucket so that would be one way to get more granularity within a resource. More info on object tagging [here](https://docs.aws.amazon.com/AmazonS3/latest/dev/object-tagging.html). + +## Cost Allocation Tags + +Tags are labels that you can assign to an AWS resource, each with a key and value. You can use cost allocation tags to track your AWS costs on a detailed level. You can tag resources through the AWS UI by using the Tags tab on the relevant resource screen. You can also add tags progrmatically via the relevant cloud product API. + +After tagging your resources (either through UI or API), you still need to activate the tags through the UI. Once tags are activated there’s a 24-hour delay until you see the data in Cost Explorer. The tags don’t apply to historical data, so you only get cost allocation for dates after the tag activation date. More info on activating tags [here](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/activating-tags.html). + +### Adding Tags + +We've looked into how tagging works for some popular AWS products, but unfortunately the methods seem to be different depending on the service. You'd also need to implement the client and get permissions for each service to be able to add tags programmatically. + +S3 Tagging: +There's currently no capability to specify a tag during bucket creation. You must first create the bucket and then perform a second action to add the tag. You can tag buckets through the AWS console, but in order to do this programmatically, you'll need to use and have access to the S3 API. Additionally, there's no way to specify multiple buckets when tagging, so you would need to make a request to tag each bucket. More on tagging buckets through the API [here](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketTagging.html). + +Similar to S3, you can add tags through the UI as well the EC2 API itself. The [`CreateTags`](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateTags.html) method allows you to tag multiple resources (up to a 1000) at a time. You can also specify tags when launching instances or include them in the launch templates. More on EC2 tagging [here](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html). + +## [GetCostAndUsageCommand Parameters](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetCostAndUsage.html#API_GetCostAndUsage_RequestParameters) + +1. TimePeriod (required): + Expects an inclusive `Start` and exclusive `End` date in YYYY-MM-DD format. + +2. Metrics (required): + Valid values: AmortizedCost, BlendedCost, NetAmortizedCost, NetUnblendedCost, NormalizedUsageAmount, UnblendedCost, and UsageQuantity + + While you can choose any cost dataset, we've used 'UnblendedCost' in the examples above as it is used by the majority of AWS users. More information on different cost types [here](https://aws.amazon.com/blogs/aws-cost-management/understanding-your-aws-cost-datasets-a-cheat-sheet/). + +3. Granularity: + Valid values: DAILY, MONTHLY, or HOURLY. + + We've used the 'DAILY' granularity above as it allows for the most flexibility depending on the requested time period. + +4. GroupBy: + Valid values: AZ, INSTANCE_TYPE, LEGAL_ENTITY_NAME, LINKED_ACCOUNT, OPERATION, PLATFORM, PURCHASE_TYPE, SERVICE, TAGS, TENANCY, RECORD_TYPE, and USAGE_TYPE. + ` From 9e021429cfbb1e0774f595d1ddb0459677b16849 Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Thu, 25 Feb 2021 00:37:48 -0500 Subject: [PATCH 043/147] Add note about methods Signed-off-by: Brenda Sukh --- plugins/cost-insights/contrib/aws-cost-explorer-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cost-insights/contrib/aws-cost-explorer-api.md b/plugins/cost-insights/contrib/aws-cost-explorer-api.md index b70ff73ccf..2181fb1d52 100644 --- a/plugins/cost-insights/contrib/aws-cost-explorer-api.md +++ b/plugins/cost-insights/contrib/aws-cost-explorer-api.md @@ -73,7 +73,7 @@ try { ## Implementing methods on [CostInsightsApi](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts) -The Cost Explorer SDK allows for easy access to cost data for easier implementation of CostInsightsApi methods such as `getGroupDailyCost`, `getProjectDailyCost`, and `getProductInsights`. +We can use the data provided by the Cost Explorer API to implement CostInsightsApi methods such as `getGroupDailyCost`, `getProjectDailyCost`, and `getProductInsights`. You'll still need to provide external data for methods such as `getUserGroups` and `getAlerts`. ### 1. [getGroupDailyCost](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts#L93) From 0ab4d32566c14b6a994f484d1418bed368525adb Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Thu, 25 Feb 2021 10:05:44 -0500 Subject: [PATCH 044/147] Add routing to params Signed-off-by: Brenda Sukh --- plugins/cost-insights/contrib/aws-cost-explorer-api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/cost-insights/contrib/aws-cost-explorer-api.md b/plugins/cost-insights/contrib/aws-cost-explorer-api.md index 2181fb1d52..5b5e6d32b6 100644 --- a/plugins/cost-insights/contrib/aws-cost-explorer-api.md +++ b/plugins/cost-insights/contrib/aws-cost-explorer-api.md @@ -51,7 +51,7 @@ const client = new CostExplorerClient({ }); ``` -2. Initiate a command with the relevant input params. The SDK provides a variety of commands, but you can access most cost data using the [GetCostAndUsageCommand](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetCostAndUsage.html) command. You can find more information on the required input params for each command [here](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_Operations_AWS_Cost_Explorer_Service.html). +2. Initiate a command with the relevant input params. The SDK provides a variety of commands, but you can access most cost data using the [GetCostAndUsageCommand](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetCostAndUsage.html) commands with the relevant [parameters](#GetCostAndUsageCommand-Parameters). ```ts const command = new GetCostAndUsageCommand(params); @@ -133,7 +133,7 @@ You can get grouped daily costs from the API by using the `GetCostAndUsageComman Sample command: ```ts -new GetCostAndUsageCommand({ +const command = new GetCostAndUsageCommand({ TimePeriod: { Start: '2020-12-01', End: '2021-01-02' }, Metrics: ['Unblended Cost'], Filter: { From 1f701d2acc0a50df83ca90cdda56a569abef612f Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Thu, 25 Feb 2021 16:23:04 -0500 Subject: [PATCH 045/147] Add vocab Signed-off-by: Brenda Sukh --- .github/styles/vocab.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 831df202e1..d2e437c984 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -207,6 +207,7 @@ onboarding pagerduty pageview parallelization +params plantuml postgres postpack @@ -276,6 +277,7 @@ validators varchar winston www +xlarge xyz yaml zoomable From 67f47a828281ffa95b22452443b1493190bfdf27 Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Thu, 25 Feb 2021 16:23:30 -0500 Subject: [PATCH 046/147] Update wording based on feedback Signed-off-by: Brenda Sukh --- plugins/cost-insights/contrib/aws-cost-explorer-api.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/plugins/cost-insights/contrib/aws-cost-explorer-api.md b/plugins/cost-insights/contrib/aws-cost-explorer-api.md index 5b5e6d32b6..562388c19d 100644 --- a/plugins/cost-insights/contrib/aws-cost-explorer-api.md +++ b/plugins/cost-insights/contrib/aws-cost-explorer-api.md @@ -1,9 +1,8 @@ # Using the AWS Cost Explorer API with Cost Insights -Cost Insights implementers are currently expected to write their own Cost Insights client to interact with the frontend plugin. We've looked into short-term solutions which we've since punted on, but wanted to share our findings from our exploration of the AWS Cost Explorer API. +Cost Insights currently does not provide a CostInsightsApi client out of the box, this is left up to the implementr. We plan to provide an open-source client and backend eventually that can pull cost data from different cloud providers. In the meantime, we briefly explored the AWS Cost Explorer API and wanted to share our findings. Please contribute to this documentation if you try any experiments with AWS Cost Explorer. -**Note:** Each request using the Cost Explorer API will incur a cost of \$0.01. -` +**Note:** Each request using the Cost Explorer API will incur a cost of \$0.01. If you anticipate high usage, adding some caching of Cost Explorer API responses could prove worthwhile over time. ## Authentication @@ -31,7 +30,7 @@ Cost Explorer permission policy: ## Setup -1. Install the AWS Cost Explorer SDK. The AWS docs recommend using the SDK over making calls to the API directly as it simplifies authentication and provides direct access to commands. +Install the AWS Cost Explorer SDK. The AWS docs recommend using the SDK over making calls to the API directly as it simplifies authentication and provides direct access to commands. ```bash yarn add @aws-sdk/client-cost-explorer @@ -297,7 +296,7 @@ There are a couple of options for breaking down costs for a resource. ## Cost Allocation Tags -Tags are labels that you can assign to an AWS resource, each with a key and value. You can use cost allocation tags to track your AWS costs on a detailed level. You can tag resources through the AWS UI by using the Tags tab on the relevant resource screen. You can also add tags progrmatically via the relevant cloud product API. +Tags are labels that you can assign to an AWS resource, each with a key and value. You can use cost allocation tags to track your AWS costs on a detailed level. You can tag resources through the AWS UI by using the Tags tab on the relevant resource screen. You can also add tags programatically via the relevant cloud product API. After tagging your resources (either through UI or API), you still need to activate the tags through the UI. Once tags are activated there’s a 24-hour delay until you see the data in Cost Explorer. The tags don’t apply to historical data, so you only get cost allocation for dates after the tag activation date. More info on activating tags [here](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/activating-tags.html). @@ -327,4 +326,3 @@ Similar to S3, you can add tags through the UI as well the EC2 API itself. The [ 4. GroupBy: Valid values: AZ, INSTANCE_TYPE, LEGAL_ENTITY_NAME, LINKED_ACCOUNT, OPERATION, PLATFORM, PURCHASE_TYPE, SERVICE, TAGS, TENANCY, RECORD_TYPE, and USAGE_TYPE. - ` From 3b1a0715c239e8355b9f3fab7edbd726bab3d94a Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Fri, 26 Feb 2021 12:01:48 -0500 Subject: [PATCH 047/147] Add reference to doc in main README Signed-off-by: Brenda Sukh --- plugins/cost-insights/README.md | 2 ++ plugins/cost-insights/contrib/aws-cost-explorer-api.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/cost-insights/README.md b/plugins/cost-insights/README.md index c10d483f3f..a103b58a77 100644 --- a/plugins/cost-insights/README.md +++ b/plugins/cost-insights/README.md @@ -25,6 +25,8 @@ yarn add @backstage/plugin-cost-insights 2. Create a CostInsights client. Clients must implement the CostInsightsApi interface. See the [API file](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts) for required methods and documentation. +**Note:** We've briefly explored using the AWS Cost Explorer API to implement a CostInsights client. Learn more about our findings [here](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/contrib/aws-cost-explorer-api.md). + ```ts // path/to/CostInsightsClient.ts import { CostInsightsApi } from '@backstage/plugin-cost-insights'; diff --git a/plugins/cost-insights/contrib/aws-cost-explorer-api.md b/plugins/cost-insights/contrib/aws-cost-explorer-api.md index 562388c19d..9b9339d348 100644 --- a/plugins/cost-insights/contrib/aws-cost-explorer-api.md +++ b/plugins/cost-insights/contrib/aws-cost-explorer-api.md @@ -1,6 +1,6 @@ # Using the AWS Cost Explorer API with Cost Insights -Cost Insights currently does not provide a CostInsightsApi client out of the box, this is left up to the implementr. We plan to provide an open-source client and backend eventually that can pull cost data from different cloud providers. In the meantime, we briefly explored the AWS Cost Explorer API and wanted to share our findings. Please contribute to this documentation if you try any experiments with AWS Cost Explorer. +Cost Insights currently does not provide a CostInsightsApi client out of the box, this is left up to the implementer. We plan to provide an open-source client and backend eventually that can pull cost data from different cloud providers. In the meantime, we briefly explored the AWS Cost Explorer API and wanted to share our findings. Please contribute to this documentation if you try any experiments with AWS Cost Explorer. **Note:** Each request using the Cost Explorer API will incur a cost of \$0.01. If you anticipate high usage, adding some caching of Cost Explorer API responses could prove worthwhile over time. From dcb20703d36e05e0db75cf41e976baf951ed4854 Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Mon, 1 Mar 2021 09:37:26 -0500 Subject: [PATCH 048/147] Add vocab Signed-off-by: Brenda Sukh --- .github/styles/vocab.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index d2e437c984..d4fb76e8d2 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -216,6 +216,7 @@ prebaked preconfigured prepack productional +programatically proxying pygments pymdownx From 2e15510a9459af3b11ba5f90da56867f96826de7 Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Mon, 1 Mar 2021 13:16:27 -0500 Subject: [PATCH 049/147] Fix spelling Signed-off-by: Brenda Sukh --- plugins/cost-insights/contrib/aws-cost-explorer-api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/cost-insights/contrib/aws-cost-explorer-api.md b/plugins/cost-insights/contrib/aws-cost-explorer-api.md index 9b9339d348..fa4d789b01 100644 --- a/plugins/cost-insights/contrib/aws-cost-explorer-api.md +++ b/plugins/cost-insights/contrib/aws-cost-explorer-api.md @@ -50,7 +50,7 @@ const client = new CostExplorerClient({ }); ``` -2. Initiate a command with the relevant input params. The SDK provides a variety of commands, but you can access most cost data using the [GetCostAndUsageCommand](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetCostAndUsage.html) commands with the relevant [parameters](#GetCostAndUsageCommand-Parameters). +2. Initiate a command with the relevant input parameters. The SDK provides a variety of commands, but you can access most cost data using the [GetCostAndUsageCommand](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetCostAndUsage.html) commands with the relevant [parameters](#GetCostAndUsageCommand-Parameters). ```ts const command = new GetCostAndUsageCommand(params); @@ -296,7 +296,7 @@ There are a couple of options for breaking down costs for a resource. ## Cost Allocation Tags -Tags are labels that you can assign to an AWS resource, each with a key and value. You can use cost allocation tags to track your AWS costs on a detailed level. You can tag resources through the AWS UI by using the Tags tab on the relevant resource screen. You can also add tags programatically via the relevant cloud product API. +Tags are labels that you can assign to an AWS resource, each with a key and value. You can use cost allocation tags to track your AWS costs on a detailed level. You can tag resources through the AWS UI by using the Tags tab on the relevant resource screen. You can also add tags programmatically via the relevant cloud product API. After tagging your resources (either through UI or API), you still need to activate the tags through the UI. Once tags are activated there’s a 24-hour delay until you see the data in Cost Explorer. The tags don’t apply to historical data, so you only get cost allocation for dates after the tag activation date. More info on activating tags [here](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/activating-tags.html). From 93ac1716d551c9c170dad1206491ac2ec3c73a51 Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Wed, 3 Mar 2021 12:19:21 -0500 Subject: [PATCH 050/147] Remove updates to vocab.txt Signed-off-by: Brenda Sukh --- .github/styles/vocab.txt | 3 --- plugins/cost-insights/contrib/aws-cost-explorer-api.md | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index d4fb76e8d2..831df202e1 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -207,7 +207,6 @@ onboarding pagerduty pageview parallelization -params plantuml postgres postpack @@ -216,7 +215,6 @@ prebaked preconfigured prepack productional -programatically proxying pygments pymdownx @@ -278,7 +276,6 @@ validators varchar winston www -xlarge xyz yaml zoomable diff --git a/plugins/cost-insights/contrib/aws-cost-explorer-api.md b/plugins/cost-insights/contrib/aws-cost-explorer-api.md index fa4d789b01..10622b7917 100644 --- a/plugins/cost-insights/contrib/aws-cost-explorer-api.md +++ b/plugins/cost-insights/contrib/aws-cost-explorer-api.md @@ -290,7 +290,7 @@ Sample response: There are a couple of options for breaking down costs for a resource. -1. We can use `USAGE_TYPE` (units used to measure usage for each service) supported by the `GroupBy` field. This requires the lowest amount of effort as we can easily add it to the existing resource request. Examples usage types for EC2: BoxUsage:c1.medium(Hrs), BoxUsage:m3.xlarge(Hrs), and BoxUsage:t1.micro(Hrs) +1. We can use `USAGE_TYPE` (units used to measure usage for each service) supported by the `GroupBy` field. This requires the lowest amount of effort as we can easily add it to the existing resource request. Examples usage types for EC2: `BoxUsage:c1.medium(Hrs)`, `BoxUsage:m3.xlarge(Hrs)`, and `BoxUsage:t1.micro(Hrs)` 2. Similar to resources, we can use custom tags to group the data. For example, you’re able to tag objects within a bucket so that would be one way to get more granularity within a resource. More info on object tagging [here](https://docs.aws.amazon.com/AmazonS3/latest/dev/object-tagging.html). From 9d455f69a8de1fca3ad1cee8e218d24f56686528 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 22 Feb 2021 16:36:57 +0100 Subject: [PATCH 051/147] Introduce parameters for namespace, kind, and name to `entityRouteRef` Signed-off-by: Oliver Sand --- .changeset/soft-months-invite.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/soft-months-invite.md diff --git a/.changeset/soft-months-invite.md b/.changeset/soft-months-invite.md new file mode 100644 index 0000000000..a6a20973e9 --- /dev/null +++ b/.changeset/soft-months-invite.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Introduce parameters for namespace, kind, and name to `entityRouteRef`. From cfc83cac16099cd53f9156df4d8fc85e6b278c13 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 22 Feb 2021 16:40:29 +0100 Subject: [PATCH 052/147] Introduce external route for linking to the entity page from the explore plugin Signed-off-by: Oliver Sand --- .changeset/large-eggs-help.md | 19 +++++++++++++++++++ packages/app/src/App.tsx | 5 ++++- .../components/DomainCard/DomainCard.test.tsx | 17 +++++++++++------ .../src/components/DomainCard/DomainCard.tsx | 12 ++++-------- .../DomainCard/DomainCardGrid.test.tsx | 17 +++++++++++------ .../DomainExplorerContent.test.tsx | 16 ++++++++++++++++ plugins/explore/src/plugin.ts | 5 ++++- plugins/explore/src/routes.ts | 7 ++++++- 8 files changed, 75 insertions(+), 23 deletions(-) create mode 100644 .changeset/large-eggs-help.md diff --git a/.changeset/large-eggs-help.md b/.changeset/large-eggs-help.md new file mode 100644 index 0000000000..e310ad7645 --- /dev/null +++ b/.changeset/large-eggs-help.md @@ -0,0 +1,19 @@ +--- +'@backstage/plugin-explore': minor +--- + +Introduce external route for linking to the entity page from the explore plugin. + +To use the explore plugin you have to bind the external route in your app: + +```typescript +const app = createApp({ + ... + bindRoutes({ bind }) { + ... + bind(explorePlugin.externalRoutes, { + catalogEntity: catalogPlugin.routes.catalogEntity, + }); + }, +}); +``` diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 5b6f7b95fe..7acde03953 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -33,7 +33,7 @@ import { CostInsightsPage, CostInsightsProjectGrowthInstructionsPage, } from '@backstage/plugin-cost-insights'; -import { ExplorePage } from '@backstage/plugin-explore'; +import { ExplorePage, explorePlugin } from '@backstage/plugin-explore'; import { GcpProjectsPage } from '@backstage/plugin-gcp-projects'; import { GraphiQLPage } from '@backstage/plugin-graphiql'; import { LighthousePage } from '@backstage/plugin-lighthouse'; @@ -79,6 +79,9 @@ const app = createApp({ bind(apiDocsPlugin.externalRoutes, { createComponent: scaffolderPlugin.routes.root, }); + bind(explorePlugin.externalRoutes, { + catalogEntity: catalogPlugin.routes.catalogEntity, + }); }, }); diff --git a/plugins/explore/src/components/DomainCard/DomainCard.test.tsx b/plugins/explore/src/components/DomainCard/DomainCard.test.tsx index 9d409051f6..56a02a3642 100644 --- a/plugins/explore/src/components/DomainCard/DomainCard.test.tsx +++ b/plugins/explore/src/components/DomainCard/DomainCard.test.tsx @@ -15,13 +15,13 @@ */ import { DomainEntity } from '@backstage/catalog-model'; -import { render } from '@testing-library/react'; +import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; +import { catalogEntityRouteRef } from '../../routes'; import { DomainCard } from './DomainCard'; describe('', () => { - it('renders a domain card', () => { + it('renders a domain card', async () => { const entity: DomainEntity = { apiVersion: 'backstage.io/v1alpha1', kind: 'Domain', @@ -34,9 +34,14 @@ describe('', () => { owner: 'guest', }, }; - const { getByText } = render(, { - wrapper: MemoryRouter, - }); + const { getByText } = await renderInTestApp( + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': catalogEntityRouteRef, + }, + }, + ); expect(getByText('artists')).toBeInTheDocument(); expect(getByText('Everything about artists')).toBeInTheDocument(); diff --git a/plugins/explore/src/components/DomainCard/DomainCard.tsx b/plugins/explore/src/components/DomainCard/DomainCard.tsx index e5efc543b2..67bee13696 100644 --- a/plugins/explore/src/components/DomainCard/DomainCard.tsx +++ b/plugins/explore/src/components/DomainCard/DomainCard.tsx @@ -14,15 +14,14 @@ * limitations under the License. */ import { DomainEntity, RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { ItemCard } from '@backstage/core'; +import { ItemCard, useRouteRef } from '@backstage/core'; import { EntityRefLinks, - entityRoute, entityRouteParams, getEntityRelations, } from '@backstage/plugin-catalog-react'; import React from 'react'; -import { generatePath } from 'react-router-dom'; +import { catalogEntityRouteRef } from '../../routes'; type DomainCardProps = { entity: DomainEntity; @@ -30,6 +29,7 @@ type DomainCardProps = { export const DomainCard = ({ entity }: DomainCardProps) => { const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); + const catalogEntityRoute = useRouteRef(catalogEntityRouteRef); return ( { /> } label="Explore" - // TODO: Use useRouteRef here to generate the path - href={generatePath( - `/catalog/${entityRoute.path}`, - entityRouteParams(entity), - )} + href={catalogEntityRoute(entityRouteParams(entity))} /> ); }; diff --git a/plugins/explore/src/components/DomainCard/DomainCardGrid.test.tsx b/plugins/explore/src/components/DomainCard/DomainCardGrid.test.tsx index bad3ca581b..ee06a50427 100644 --- a/plugins/explore/src/components/DomainCard/DomainCardGrid.test.tsx +++ b/plugins/explore/src/components/DomainCard/DomainCardGrid.test.tsx @@ -15,13 +15,13 @@ */ import { DomainEntity } from '@backstage/catalog-model'; -import { render } from '@testing-library/react'; +import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; +import { catalogEntityRouteRef } from '../../routes'; import { DomainCardGrid } from './DomainCardGrid'; describe('', () => { - it('renders a grid of domain cards', () => { + it('renders a grid of domain cards', async () => { const entities: DomainEntity[] = [ { apiVersion: 'backstage.io/v1alpha1', @@ -44,9 +44,14 @@ describe('', () => { }, }, ]; - const { getByText } = render(, { - wrapper: MemoryRouter, - }); + const { getByText } = await renderInTestApp( + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': catalogEntityRouteRef, + }, + }, + ); expect(getByText('artists')).toBeInTheDocument(); expect(getByText('playback')).toBeInTheDocument(); diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx index f29ad2cd6a..cd3f2847c6 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx @@ -20,6 +20,7 @@ import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; +import { catalogEntityRouteRef } from '../../routes'; import { DomainExplorerContent } from './DomainExplorerContent'; describe('', () => { @@ -71,6 +72,11 @@ describe('', () => { , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': catalogEntityRouteRef, + }, + }, ); await waitFor(() => { @@ -86,6 +92,11 @@ describe('', () => { , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': catalogEntityRouteRef, + }, + }, ); await waitFor(() => @@ -101,6 +112,11 @@ describe('', () => { , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': catalogEntityRouteRef, + }, + }, ); await waitFor(() => diff --git a/plugins/explore/src/plugin.ts b/plugins/explore/src/plugin.ts index 27573eaeb9..c5dd6644e7 100644 --- a/plugins/explore/src/plugin.ts +++ b/plugins/explore/src/plugin.ts @@ -16,7 +16,7 @@ import { createApiFactory, createPlugin } from '@backstage/core'; import { exploreToolsConfigRef } from '@backstage/plugin-explore-react'; -import { exploreRouteRef } from './routes'; +import { catalogEntityRouteRef, exploreRouteRef } from './routes'; import { exampleTools } from './util/examples'; export const explorePlugin = createPlugin({ @@ -37,4 +37,7 @@ export const explorePlugin = createPlugin({ routes: { explore: exploreRouteRef, }, + externalRoutes: { + catalogEntity: catalogEntityRouteRef, + }, }); diff --git a/plugins/explore/src/routes.ts b/plugins/explore/src/routes.ts index c41a53128f..fc7868c4c9 100644 --- a/plugins/explore/src/routes.ts +++ b/plugins/explore/src/routes.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createRouteRef } from '@backstage/core'; +import { createExternalRouteRef, createRouteRef } from '@backstage/core'; const NoIcon = () => null; @@ -22,3 +22,8 @@ export const exploreRouteRef = createRouteRef({ icon: NoIcon, title: 'Explore', }); + +export const catalogEntityRouteRef = createExternalRouteRef({ + id: 'catalog-entity', + params: ['namespace', 'kind', 'name'], +}); From 962884cbbcf5e3b16528b95b7a6a5c6fb8bb2ceb Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Wed, 3 Mar 2021 15:20:58 -0500 Subject: [PATCH 053/147] Reword intro paragraph Signed-off-by: Adam Harvey --- docs/dls/contributing-to-storybook.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dls/contributing-to-storybook.md b/docs/dls/contributing-to-storybook.md index 26efd30eda..fff9e51e18 100644 --- a/docs/dls/contributing-to-storybook.md +++ b/docs/dls/contributing-to-storybook.md @@ -4,8 +4,8 @@ title: Contributing to Storybook description: Documentation on How to Contribute to Storybook --- -Storybook is an open source tool to provide users a way to explore the Backstage -UI elements and how to use them in developing the Backstage core and its +The Backstage Storybook provides you a way to explore reusable Backstage User +Interface elements and how to use them in developing the Backstage core and its plugins. These UI elements are commonly called "components", and include things such as buttons, tables, specialized widgets with specific formatting, and so on. From fde006b434ca9523a06fb91d41753d555e97dda1 Mon Sep 17 00:00:00 2001 From: Remi Date: Thu, 25 Feb 2021 22:56:01 +0100 Subject: [PATCH 054/147] feat(splunk-on-call-plugin): add REST endpoint Signed-off-by: Remi --- plugins/splunk-on-call/README.md | 1 + plugins/splunk-on-call/src/api/client.ts | 77 ++---- plugins/splunk-on-call/src/api/types.ts | 46 ++-- .../components/Incident/IncidentListItem.tsx | 63 +++-- .../src/components/Incident/Incidents.tsx | 5 + .../src/components/SplunkOnCallCard.tsx | 23 +- .../TriggerDialog/TriggerDialog.tsx | 255 +++++------------- .../splunk-on-call/src/components/types.ts | 12 +- 8 files changed, 177 insertions(+), 305 deletions(-) diff --git a/plugins/splunk-on-call/README.md b/plugins/splunk-on-call/README.md index 73ee05c181..89fd45adc0 100644 --- a/plugins/splunk-on-call/README.md +++ b/plugins/splunk-on-call/README.md @@ -56,6 +56,7 @@ In `app-config.yaml`: ```yaml splunkOnCall: username: + eventsRestEndpoint: ``` The user supplied must be a valid Splunk On-Call user and a member of your organization. diff --git a/plugins/splunk-on-call/src/api/client.ts b/plugins/splunk-on-call/src/api/client.ts index 501dad80a2..616ebb7023 100644 --- a/plugins/splunk-on-call/src/api/client.ts +++ b/plugins/splunk-on-call/src/api/client.ts @@ -31,7 +31,6 @@ import { RequestOptions, ListUserResponse, EscalationPolicyResponse, - PatchIncidentRequest, } from './types'; export class UnauthorizedError extends Error {} @@ -45,8 +44,11 @@ export class SplunkOnCallClient implements SplunkOnCallApi { static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) { const usernameFromConfig: string | null = configApi.getOptionalString('splunkOnCall.username') || null; + const eventsRestEndpoint: string | null = + configApi.getOptionalString('splunkOnCall.eventsRestEndpoint') || null; return new SplunkOnCallClient({ username: usernameFromConfig, + eventsRestEndpoint, discoveryApi, }); } @@ -80,50 +82,6 @@ export class SplunkOnCallClient implements SplunkOnCallApi { return teams; } - async acknowledgeIncident({ - incidentNames, - }: PatchIncidentRequest): Promise { - const options = { - method: 'PATCH', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - userName: this.config.username, - incidentNames, - }), - }; - - const url = `${await this.config.discoveryApi.getBaseUrl( - 'proxy', - )}/splunk-on-call/v1/incidents/ack`; - - return this.request(url, options); - } - - async resolveIncident({ - incidentNames, - }: PatchIncidentRequest): Promise { - const options = { - method: 'PATCH', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - userName: this.config.username, - incidentNames, - }), - }; - - const url = `${await this.config.discoveryApi.getBaseUrl( - 'proxy', - )}/splunk-on-call/v1/incidents/resolve`; - - return this.request(url, options); - } - async getUsers(): Promise { const url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', @@ -142,19 +100,22 @@ export class SplunkOnCallClient implements SplunkOnCallApi { return policies; } - async triggerAlarm({ - summary, - details, - userName, - targets, - isMultiResponder, + async incidentAction({ + routingKey, + incidentType, + incidentId, + incidentDisplayName, + incidentMessage, + incidentStartTime, }: TriggerAlarmRequest): Promise { const body = JSON.stringify({ - summary, - details, - userName: this.config.username || userName, - targets, - isMultiResponder, + message_type: incidentType, + ...(incidentId ? { entity_id: incidentId } : {}), + ...(incidentDisplayName + ? { entity_display_name: incidentDisplayName } + : {}), + ...(incidentMessage ? { state_message: incidentMessage } : {}), + ...(incidentStartTime ? { state_start_time: incidentStartTime } : {}), }); const options = { @@ -166,9 +127,7 @@ export class SplunkOnCallClient implements SplunkOnCallApi { body, }; - const url = `${await this.config.discoveryApi.getBaseUrl( - 'proxy', - )}/splunk-on-call/v1/incidents`; + const url = `${this.config.eventsRestEndpoint}/${routingKey}`; return this.request(url, options); } diff --git a/plugins/splunk-on-call/src/api/types.ts b/plugins/splunk-on-call/src/api/types.ts index 500f2a6291..b4007a9051 100644 --- a/plugins/splunk-on-call/src/api/types.ts +++ b/plugins/splunk-on-call/src/api/types.ts @@ -23,22 +23,20 @@ import { } from '../components/types'; import { DiscoveryApi } from '@backstage/core'; -export enum TargetType { - UserValue = 'User', - EscalationPolicyValue = 'EscalationPolicy', -} - -export type IncidentTarget = { - type: TargetType; - slug: string; -}; +export type MessageType = + | 'CRITICAL' + | 'WARNING' + | 'ACKNOWLEDGEMENT' + | 'INFO' + | 'RECOVERY'; export type TriggerAlarmRequest = { - targets: IncidentTarget[]; - details: string; - summary: string; - userName: string; - isMultiResponder?: boolean; + routingKey?: string; + incidentType: MessageType; + incidentId?: string; + incidentDisplayName?: string; + incidentMessage?: string; + incidentStartTime?: number; }; export interface SplunkOnCallApi { @@ -53,19 +51,9 @@ export interface SplunkOnCallApi { getOnCallUsers(): Promise; /** - * Triggers an incident to specific users and/or specific teams. + * Triggers-Resolves-Acknowledge an incident. */ - triggerAlarm(request: TriggerAlarmRequest): Promise; - - /** - * Resolves an incident. - */ - resolveIncident(request: PatchIncidentRequest): Promise; - - /** - * Acknowledge an incident. - */ - acknowledgeIncident(request: PatchIncidentRequest): Promise; + incidentAction(request: TriggerAlarmRequest): Promise; /** * Get a list of users for your organization. @@ -83,11 +71,6 @@ export interface SplunkOnCallApi { getEscalationPolicies(): Promise; } -export type PatchIncidentRequest = { - incidentNames: string[]; - message?: string; -}; - export type EscalationPolicyResponse = { policies: EscalationPolicyInfo[]; }; @@ -107,6 +90,7 @@ export type OnCallsResponse = { export type ClientApiConfig = { username: string | null; + eventsRestEndpoint: string | null; discoveryApi: DiscoveryApi; }; diff --git a/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx b/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx index 1bf233de86..7bd8081226 100644 --- a/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx +++ b/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx @@ -39,7 +39,7 @@ import { Incident, IncidentPhase } from '../types'; import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; import { splunkOnCallApiRef } from '../../api/client'; import { useAsyncFn } from 'react-use'; -import { PatchIncidentRequest } from '../../api/types'; +import { TriggerAlarmRequest } from '../../api/types'; const useStyles = makeStyles({ denseListIcon: { @@ -61,6 +61,7 @@ const useStyles = makeStyles({ }); type Props = { + team: string; incident: Incident; onIncidentAction: () => void; }; @@ -93,20 +94,24 @@ const incidentPhaseTooltip = (currentPhase: IncidentPhase) => { const IncidentAction = ({ currentPhase, - incidentNames, + incidentId, resolveAction, acknowledgeAction, }: { currentPhase: string; - incidentNames: string[]; - resolveAction: (args: PatchIncidentRequest) => void; - acknowledgeAction: (args: PatchIncidentRequest) => void; + incidentId: string; + resolveAction: (args: TriggerAlarmRequest) => void; + acknowledgeAction: (args: TriggerAlarmRequest) => void; }) => { switch (currentPhase) { case 'UNACKED': return ( - acknowledgeAction({ incidentNames })}> + + acknowledgeAction({ incidentId, incidentType: 'ACKNOWLEDGEMENT' }) + } + > @@ -114,7 +119,11 @@ const IncidentAction = ({ case 'ACKED': return ( - resolveAction({ incidentNames })}> + + resolveAction({ incidentId, incidentType: 'RECOVERY' }) + } + > @@ -124,7 +133,11 @@ const IncidentAction = ({ } }; -export const IncidentListItem = ({ incident, onIncidentAction }: Props) => { +export const IncidentListItem = ({ + incident, + onIncidentAction, + team, +}: Props) => { const classes = useStyles(); const duration = new Date().getTime() - new Date(incident.startTime!).getTime(); @@ -136,17 +149,26 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => { const hasBeenManuallyTriggered = incident.monitorName?.includes('vouser-'); - const user = hasBeenManuallyTriggered - ? incident.monitorName?.replace('vouser-', '') - : incident.monitorName; + const source = () => { + if (hasBeenManuallyTriggered) { + return incident.monitorName?.replace('vouser-', ''); + } + if (incident.monitorType === 'API') { + return '{ REST }'; + } + + return incident.monitorName; + }; const [ { value: resolveValue, error: resolveError }, handleResolveIncident, ] = useAsyncFn( - async ({ incidentNames }: PatchIncidentRequest) => - await api.resolveIncident({ - incidentNames, + async ({ incidentId, incidentType }: TriggerAlarmRequest) => + await api.incidentAction({ + routingKey: team, + incidentType, + incidentId, }), ); @@ -154,9 +176,11 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => { { value: acknowledgeValue, error: acknowledgeError }, handleAcknowledgeIncident, ] = useAsyncFn( - async ({ incidentNames }: PatchIncidentRequest) => - await api.acknowledgeIncident({ - incidentNames, + async ({ incidentId, incidentType }: TriggerAlarmRequest) => + await api.incidentAction({ + routingKey: team, + incidentType, + incidentId, }), ); @@ -211,7 +235,8 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => { }} secondary={ - Created {createdAt} {user && `by ${user}`} + #{incident.incidentNumber} - Created {createdAt}{' '} + {source() && `by ${source()}`} } /> @@ -220,7 +245,7 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => { diff --git a/plugins/splunk-on-call/src/components/Incident/Incidents.tsx b/plugins/splunk-on-call/src/components/Incident/Incidents.tsx index e1a79314b9..507020f0a5 100644 --- a/plugins/splunk-on-call/src/components/Incident/Incidents.tsx +++ b/plugins/splunk-on-call/src/components/Incident/Incidents.tsx @@ -33,6 +33,10 @@ export const Incidents = ({ refreshIncidents, team }: Props) => { const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn( async () => { + // For some reason the changes applied to incidents (trigger-resolve-acknowledge) + // may take some time to actually be applied after receiving the response from the server. + // The timeout compensates for this latency. + await new Promise(resolve => setTimeout(resolve, 2000)); const allIncidents = await api.getIncidents(); const teams = await api.getTeams(); const teamSlug = teams.find(teamValue => teamValue.name === team)?.slug; @@ -71,6 +75,7 @@ export const Incidents = ({ refreshIncidents, team }: Props) => { getIncidents()} key={index} + team={team} incident={incident} /> ))} diff --git a/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx index 84d94d60d3..e2cc642c6a 100644 --- a/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx @@ -74,6 +74,16 @@ export const MissingUsername = () => ( ); +export const MissingEventsRestEndpoint = () => ( + + + +); + export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_TEAM]); @@ -89,7 +99,10 @@ export const SplunkOnCallCard = ({ entity }: Props) => { const [refreshIncidents, setRefreshIncidents] = useState(false); const team = entity.metadata.annotations![SPLUNK_ON_CALL_TEAM]; - const username = config.getOptionalString('splunkOnCall.username'); + const username = config.getOptionalString('splunkOnCall.username') || null; + + const eventsRestEndpoint = + config.getOptionalString('splunkOnCall.eventsRestEndpoint') || null; const handleRefresh = useCallback(() => { setRefreshIncidents(x => !x); @@ -140,15 +153,19 @@ export const SplunkOnCallCard = ({ entity }: Props) => { return ; } + if (!eventsRestEndpoint) { + return ; + } + return ( <> {users?.usersHashMap && team && ( )} - {users && incidentCreator && ( + {incidentCreator && ( void; @@ -79,7 +69,7 @@ const useStyles = makeStyles((theme: Theme) => ); export const TriggerDialog = ({ - users, + team, incidentCreator, showDialog, handleDialog, @@ -89,72 +79,46 @@ export const TriggerDialog = ({ const api = useApi(splunkOnCallApiRef); const classes = useStyles(); - const [userTargets, setUserTargets] = useState([]); - const [policyTargets, setPolicyTargets] = useState([]); - const [detailsValue, setDetails] = useState(''); - const [summaryValue, setSummary] = useState(''); - const [isMultiResponderValue, setIsMultiResponder] = useState('1'); + const [incidentType, setIncidentType] = useState(''); + const [incidentId, setIncidentId] = useState(); + const [incidentDisplayName, setIncidentDisplayName] = useState(''); + const [incidentMessage, setIncidentMessage] = useState(''); + const [incidentStartTime, setIncidentStartTime] = useState(); const [ { value, loading: triggerLoading, error: triggerError }, handleTriggerAlarm, ] = useAsyncFn( - async ( - summary: string, - details: string, - userName: string, - targets: IncidentTarget[], - isMultiResponder: boolean, - ) => - await api.triggerAlarm({ - summary, - details, - userName, - targets, - isMultiResponder, - }), + async (params: TriggerAlarmRequest) => await api.incidentAction(params), ); - const { - value: policies, - loading: policiesLoaading, - error: policiesError, - } = useAsync(async () => { - const allPolicies = await api.getEscalationPolicies(); - return allPolicies; - }); - - const handleUserTargets = (event: React.ChangeEvent<{ value: unknown }>) => { - setUserTargets(event.target.value as string[]); + const handleIncidentType = (event: React.ChangeEvent<{ value: unknown }>) => { + setIncidentType(event.target.value as string); }; - const handlePolicyTargets = ( - event: React.ChangeEvent<{ value: unknown }>, + const handleIncidentId = (event: React.ChangeEvent) => { + setIncidentId(event.target.value as string); + }; + + const handleIncidentDisplayName = ( + event: React.ChangeEvent, ) => { - setPolicyTargets(event.target.value as string[]); + setIncidentDisplayName(event.target.value); }; - const detailsChanged = (event: React.ChangeEvent) => { - setDetails(event.target.value); - }; - - const summaryChanged = (event: React.ChangeEvent) => { - setSummary(event.target.value); - }; - - const isMultiResponderChanged = ( - event: React.ChangeEvent<{ value: unknown }>, + const handleIncidentMessage = ( + event: React.ChangeEvent, ) => { - setIsMultiResponder(event.target.value as string); + setIncidentMessage(event.target.value); }; - const targets = (): IncidentTarget[] => [ - ...userTargets.map(user => ({ slug: user, type: TargetType.UserValue })), - ...policyTargets.map(user => ({ - slug: user, - type: TargetType.EscalationPolicyValue, - })), - ]; + const handleIncidentStartTime = ( + event: React.ChangeEvent, + ) => { + const dateTime = new Date(event.target.value).getTime(); + const dateTimeInSeconds = Math.floor(dateTime / 1000); + setIncidentStartTime(dateTimeInSeconds); + }; useEffect(() => { if (value) { @@ -203,132 +167,58 @@ export const TriggerDialog = ({ automatically be amended to the alarm so that the receiver can reach out to you if necessary. -
- - Select the targets - -
- - Select Users - - - - Select Teams / Policies - - -
-
- - Acknowledge Behavior - + Incident type + + @@ -337,20 +227,21 @@ export const TriggerDialog = ({ id="trigger" color="secondary" disabled={ - !detailsValue || - !summaryValue || - (!userTargets.length && !policyTargets.length) || + !incidentType.length || + !incidentDisplayName || + !incidentMessage || triggerLoading } variant="contained" onClick={() => - handleTriggerAlarm( - summaryValue, - detailsValue, - incidentCreator.username!, - targets(), - !!Number(isMultiResponderValue), - ) + handleTriggerAlarm({ + routingKey: team, + incidentType, + incidentDisplayName, + incidentMessage, + ...(incidentId ? { incidentId } : {}), + ...(incidentStartTime ? { incidentStartTime } : {}), + } as TriggerAlarmRequest) } endIcon={triggerLoading && } > diff --git a/plugins/splunk-on-call/src/components/types.ts b/plugins/splunk-on-call/src/components/types.ts index de5718cb28..9b2af1cddd 100644 --- a/plugins/splunk-on-call/src/components/types.ts +++ b/plugins/splunk-on-call/src/components/types.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { IncidentTarget } from '../api/types'; - export type Team = { name?: string; slug?: string; @@ -68,14 +66,6 @@ export type User = { _selfUrl?: string; }; -export type CreateIncidentRequest = { - summary: string; - details: string; - userName: string; - targets: IncidentTarget; - isMultiResponder: boolean; -}; - export type IncidentPhase = 'UNACKED' | 'ACKED' | 'RESOLVED'; export type Incident = { @@ -88,7 +78,7 @@ export type Incident = { alertCount?: number; lastAlertTime?: string; lastAlertId?: string; - entityId?: string; + entityId: string; host?: string; service?: string; pagedUsers?: string[]; From 98fb840b4daabe57c5d9c43a637c73c1e56fd678 Mon Sep 17 00:00:00 2001 From: Remi Date: Thu, 25 Feb 2021 22:56:19 +0100 Subject: [PATCH 055/147] feat(splunk-on-call-plugin): REST endpoint tests Signed-off-by: Remi --- .../components/Incident/Incidents.test.tsx | 31 +++++++--- .../src/components/SplunkOnCallCard.test.tsx | 5 +- .../TriggerDialog/TriggerDialog.test.tsx | 58 ++++++------------- 3 files changed, 44 insertions(+), 50 deletions(-) diff --git a/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx b/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx index d1f973735c..d0171df985 100644 --- a/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx +++ b/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx @@ -62,7 +62,10 @@ describe('Incidents', () => { ), ); await waitFor(() => !queryByTestId('progress')); - expect(getByText('Nice! No incidents found!')).toBeInTheDocument(); + await waitFor( + () => expect(getByText('Nice! No incidents found!')).toBeInTheDocument(), + { timeout: 2000 }, + ); }); it('Renders all incidents', async () => { @@ -87,11 +90,15 @@ describe('Incidents', () => { ), ); await waitFor(() => !queryByTestId('progress')); - expect( - getByText('user', { - exact: false, - }), - ).toBeInTheDocument(); + await waitFor( + () => + expect( + getByText('user', { + exact: false, + }), + ).toBeInTheDocument(), + { timeout: 2000 }, + ); expect(getByText('test-incident')).toBeInTheDocument(); expect(getByTitle('Acknowledged')).toBeInTheDocument(); expect(getByLabelText('Status warning')).toBeInTheDocument(); @@ -113,8 +120,14 @@ describe('Incidents', () => { ), ); await waitFor(() => !queryByTestId('progress')); - expect( - getByText('Error encountered while fetching information. Error occurred'), - ).toBeInTheDocument(); + await waitFor( + () => + expect( + getByText( + 'Error encountered while fetching information. Error occurred', + ), + ).toBeInTheDocument(), + { timeout: 2000 }, + ); }); }); diff --git a/plugins/splunk-on-call/src/components/SplunkOnCallCard.test.tsx b/plugins/splunk-on-call/src/components/SplunkOnCallCard.test.tsx index b1280ab21c..3418ed3398 100644 --- a/plugins/splunk-on-call/src/components/SplunkOnCallCard.test.tsx +++ b/plugins/splunk-on-call/src/components/SplunkOnCallCard.test.tsx @@ -91,7 +91,10 @@ describe('SplunkOnCallCard', () => { ); await waitFor(() => !queryByTestId('progress')); expect(getByText('Create Incident')).toBeInTheDocument(); - expect(getByText('Nice! No incidents found!')).toBeInTheDocument(); + await waitFor( + () => expect(getByText('Nice! No incidents found!')).toBeInTheDocument(), + { timeout: 2000 }, + ); expect(getByText('Empty escalation policy')).toBeInTheDocument(); }); diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx index 741a15fb41..5308148afa 100644 --- a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx +++ b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx @@ -24,13 +24,12 @@ import { } from '@backstage/core'; import { splunkOnCallApiRef } from '../../api'; import { TriggerDialog } from './TriggerDialog'; -import { ESCALATION_POLICIES, MOCKED_USER } from '../../api/mocks'; +import { MOCKED_USER } from '../../api/mocks'; describe('TriggerDialog', () => { const mockTriggerAlarmFn = jest.fn(); const mockSplunkOnCallApi = { - triggerAlarm: mockTriggerAlarmFn, - getEscalationPolicies: async () => ESCALATION_POLICIES, + incidentAction: mockTriggerAlarmFn, }; const apis = ApiRegistry.from([ @@ -45,14 +44,13 @@ describe('TriggerDialog', () => { ]); it('open the dialog and trigger an alarm', async () => { - const { getByText, getByRole, getAllByRole, getByTestId } = render( + const { getByText, getByRole, getByTestId } = render( wrapInTestApp( {}} - users={[MOCKED_USER]} onIncidentCreated={() => {}} /> , @@ -65,36 +63,20 @@ describe('TriggerDialog', () => { exact: false, }), ).toBeInTheDocument(); - const summary = getByTestId('trigger-summary-input'); - const body = getByTestId('trigger-body-input'); - const behavior = getByTestId('trigger-select-behavior'); - const description = 'Test Trigger Alarm'; - await act(async () => { - fireEvent.change(summary, { target: { value: description } }); - fireEvent.change(body, { target: { value: description } }); - fireEvent.change(behavior, { target: { value: '0' } }); - fireEvent.mouseDown(getAllByRole('button')[0]); - }); + const incidentType = getByTestId('trigger-incident-type'); + const incidentId = getByTestId('trigger-incident-id'); + const incidentDisplayName = getByTestId('trigger-incident-displayName'); + const incidentMessage = getByTestId('trigger-incident-message'); - // Trigger user targets select - const options = getAllByRole('option'); await act(async () => { - fireEvent.click(options[0]); - fireEvent.keyDown(options[0], { - key: 'Escape', - code: 'Escape', - keyCode: 27, - charCode: 27, + fireEvent.change(incidentType, { target: { value: 'CRITICAL' } }); + fireEvent.change(incidentId, { target: { value: 'incident-id' } }); + fireEvent.change(incidentDisplayName, { + target: { value: 'incident-display-name' }, + }); + fireEvent.change(incidentMessage, { + target: { value: 'incident-message' }, }); - }); - - // Trigger policy targets select - await act(async () => { - fireEvent.mouseDown(getAllByRole('button')[1]); - }); - const policiesOptions = getAllByRole('option'); - await act(async () => { - fireEvent.click(policiesOptions[0]); }); // Trigger incident creation button @@ -104,14 +86,10 @@ describe('TriggerDialog', () => { }); expect(mockTriggerAlarmFn).toHaveBeenCalled(); expect(mockTriggerAlarmFn).toHaveBeenCalledWith({ - summary: description, - details: description, - userName: 'test_user', - targets: [ - { slug: 'test_user', type: 'User' }, - { slug: 'team-zEalMCgwYSA0Lt40', type: 'EscalationPolicy' }, - ], - isMultiResponder: false, + incidentType: 'CRITICAL', + incidentId: 'incident-id', + incidentDisplayName: 'incident-display-name', + incidentMessage: 'incident-message', }); }); }); From 14af7ac400bef36e7088b8dba949c28c8acd028f Mon Sep 17 00:00:00 2001 From: Remi Date: Thu, 25 Feb 2021 23:22:53 +0100 Subject: [PATCH 056/147] feat(splunk-on-call-plugin): update documentation Signed-off-by: Remi --- plugins/splunk-on-call/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugins/splunk-on-call/README.md b/plugins/splunk-on-call/README.md index 89fd45adc0..5276d6d2df 100644 --- a/plugins/splunk-on-call/README.md +++ b/plugins/splunk-on-call/README.md @@ -51,6 +51,9 @@ import { In order to be able to perform certain action (create-acknowledge-resolve an action), you need to provide the username of the user making the action. The user supplied must be a valid Splunk On-Call user and a member of your organization. +In addition, you also need to enable the REST Endpoint integration on https://portal.victorops.com/ by going to Integrations > 3rd Party Integrations > REST – Generic. +You can now copy the URL to notify: `/$routing_key` + In `app-config.yaml`: ```yaml @@ -88,6 +91,14 @@ annotations: splunk.com/on-call-team': ``` +### Create the Routing Key + +To be able to use the REST Endpoint seen above, you must have created a routing key with the **same name** as the provided team. + +You can create a new routing key on https://portal.victorops.com/ by going to Settings > Routing Keys. + +You can read [Create & Manage Alert Routing Keys](https://help.victorops.com/knowledge-base/routing-keys/#routing-key-tips-tricks) for further information. + ## Providing the API key and API id In order for the client to make requests to the [Splunk On-Call API](https://portal.victorops.com/public/api-docs.html#/) it needs an [API ID and an API Key](https://help.victorops.com/knowledge-base/api/). From 274b00ed52c4d0d8f037a601141adf0b14fa60b6 Mon Sep 17 00:00:00 2001 From: Remi Date: Fri, 26 Feb 2021 12:32:00 +0100 Subject: [PATCH 057/147] feat(splunk-on-call-plugin): update style + fetch Signed-off-by: Remi --- plugins/splunk-on-call/README.md | 8 +-- plugins/splunk-on-call/src/api/client.ts | 3 - plugins/splunk-on-call/src/api/types.ts | 1 - .../Escalation/EscalationPolicy.tsx | 46 ++++++++++--- .../src/components/Incident/Incidents.tsx | 60 ++++++++++++----- .../src/components/SplunkOnCallCard.tsx | 47 +++++-------- .../TriggerDialog/TriggerDialog.tsx | 67 ++++++++++--------- 7 files changed, 138 insertions(+), 94 deletions(-) diff --git a/plugins/splunk-on-call/README.md b/plugins/splunk-on-call/README.md index 5276d6d2df..78313d3329 100644 --- a/plugins/splunk-on-call/README.md +++ b/plugins/splunk-on-call/README.md @@ -48,22 +48,18 @@ import { ## Client configuration -In order to be able to perform certain action (create-acknowledge-resolve an action), you need to provide the username of the user making the action. -The user supplied must be a valid Splunk On-Call user and a member of your organization. +In order to be able to perform certain action (create-acknowledge-resolve an action), you need to provide a REST Endpoint. -In addition, you also need to enable the REST Endpoint integration on https://portal.victorops.com/ by going to Integrations > 3rd Party Integrations > REST – Generic. +To enable the REST Endpoint integration you can go on https://portal.victorops.com/ inside Integrations > 3rd Party Integrations > REST – Generic. You can now copy the URL to notify: `/$routing_key` In `app-config.yaml`: ```yaml splunkOnCall: - username: eventsRestEndpoint: ``` -The user supplied must be a valid Splunk On-Call user and a member of your organization. - In order to make the API calls, you need to provide a new proxy config which will redirect to the Splunk On-Call API endpoint and add authentication information in the headers: ```yaml diff --git a/plugins/splunk-on-call/src/api/client.ts b/plugins/splunk-on-call/src/api/client.ts index 616ebb7023..045ea04d63 100644 --- a/plugins/splunk-on-call/src/api/client.ts +++ b/plugins/splunk-on-call/src/api/client.ts @@ -42,12 +42,9 @@ export const splunkOnCallApiRef = createApiRef({ export class SplunkOnCallClient implements SplunkOnCallApi { static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) { - const usernameFromConfig: string | null = - configApi.getOptionalString('splunkOnCall.username') || null; const eventsRestEndpoint: string | null = configApi.getOptionalString('splunkOnCall.eventsRestEndpoint') || null; return new SplunkOnCallClient({ - username: usernameFromConfig, eventsRestEndpoint, discoveryApi, }); diff --git a/plugins/splunk-on-call/src/api/types.ts b/plugins/splunk-on-call/src/api/types.ts index b4007a9051..b42ab6a287 100644 --- a/plugins/splunk-on-call/src/api/types.ts +++ b/plugins/splunk-on-call/src/api/types.ts @@ -89,7 +89,6 @@ export type OnCallsResponse = { }; export type ClientApiConfig = { - username: string | null; eventsRestEndpoint: string | null; discoveryApi: DiscoveryApi; }; diff --git a/plugins/splunk-on-call/src/components/Escalation/EscalationPolicy.tsx b/plugins/splunk-on-call/src/components/Escalation/EscalationPolicy.tsx index 9538a0e73b..4234ba1621 100644 --- a/plugins/splunk-on-call/src/components/Escalation/EscalationPolicy.tsx +++ b/plugins/splunk-on-call/src/components/Escalation/EscalationPolicy.tsx @@ -15,7 +15,13 @@ */ import React from 'react'; -import { List, ListSubheader } from '@material-ui/core'; +import { + createStyles, + List, + ListSubheader, + makeStyles, + Theme, +} from '@material-ui/core'; import { EscalationUsersEmptyState } from './EscalationUsersEmptyState'; import { EscalationUser } from './EscalationUser'; import { useAsync } from 'react-use'; @@ -24,12 +30,28 @@ import { useApi, Progress } from '@backstage/core'; import { Alert } from '@material-ui/lab'; import { User } from '../types'; +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + maxHeight: '400px', + overflow: 'auto', + }, + subheader: { + backgroundColor: 'white', + }, + progress: { + margin: `0 ${theme.spacing(2)}px`, + }, + }), +); + type Props = { users: { [key: string]: User }; team: string; }; export const EscalationPolicy = ({ users, team }: Props) => { + const classes = useStyles(); const api = useApi(splunkOnCallApiRef); const { value: userNames, loading, error } = useAsync(async () => { @@ -54,24 +76,30 @@ export const EscalationPolicy = ({ users, team }: Props) => { ); } - if (loading) { - return ; - } - - if (!userNames?.length) { + if (!loading && !userNames?.length) { return ; } return ( - ON CALL}> - {userNames && + ON CALL + } + > + {loading ? ( + + ) : ( + userNames && userNames.map( (userName, index) => userName && userName in users && ( ), - )} + ) + )} ); }; diff --git a/plugins/splunk-on-call/src/components/Incident/Incidents.tsx b/plugins/splunk-on-call/src/components/Incident/Incidents.tsx index 507020f0a5..189367542d 100644 --- a/plugins/splunk-on-call/src/components/Incident/Incidents.tsx +++ b/plugins/splunk-on-call/src/components/Incident/Incidents.tsx @@ -15,7 +15,13 @@ */ import React, { useEffect } from 'react'; -import { List, ListSubheader } from '@material-ui/core'; +import { + createStyles, + List, + ListSubheader, + makeStyles, + Theme, +} from '@material-ui/core'; import { IncidentListItem } from './IncidentListItem'; import { IncidentsEmptyState } from './IncidentEmptyState'; import { useAsyncFn } from 'react-use'; @@ -23,12 +29,28 @@ import { splunkOnCallApiRef } from '../../api'; import { useApi, Progress } from '@backstage/core'; import { Alert } from '@material-ui/lab'; +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + maxHeight: '400px', + overflow: 'auto', + }, + subheader: { + backgroundColor: 'white', + }, + progress: { + margin: `0 ${theme.spacing(2)}px`, + }, + }), +); + type Props = { refreshIncidents: boolean; team: string; }; export const Incidents = ({ refreshIncidents, team }: Props) => { + const classes = useStyles(); const api = useApi(splunkOnCallApiRef); const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn( @@ -61,24 +83,32 @@ export const Incidents = ({ refreshIncidents, team }: Props) => { ); } - if (loading) { - return ; - } - - if (!incidents?.length) { + if (!loading && !incidents?.length) { return ; } return ( - INCIDENTS}> - {incidents!.map((incident, index) => ( - getIncidents()} - key={index} - team={team} - incident={incident} - /> - ))} + + CRITICAL INCIDENTS + + } + > + {loading ? ( + + ) : ( + incidents!.map((incident, index) => ( + getIncidents()} + key={index} + team={team} + incident={incident} + /> + )) + )} ); }; diff --git a/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx index e2cc642c6a..8a7364fea9 100644 --- a/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx @@ -34,6 +34,7 @@ import { } from '@material-ui/core'; import { Incidents } from './Incident'; import { EscalationPolicy } from './Escalation'; +import WebIcon from '@material-ui/icons/Web'; import { useAsync } from 'react-use'; import { Alert } from '@material-ui/lab'; import { splunkOnCallApiRef, UnauthorizedError } from '../api'; @@ -64,16 +65,6 @@ export const MissingTeamAnnotation = () => ( ); -export const MissingUsername = () => ( - - - -); - export const MissingEventsRestEndpoint = () => ( { const [refreshIncidents, setRefreshIncidents] = useState(false); const team = entity.metadata.annotations![SPLUNK_ON_CALL_TEAM]; - const username = config.getOptionalString('splunkOnCall.username') || null; - const eventsRestEndpoint = config.getOptionalString('splunkOnCall.eventsRestEndpoint') || null; @@ -126,9 +115,6 @@ export const SplunkOnCallCard = ({ entity }: Props) => { return { usersHashMap, userList: allUsers }; }); - const incidentCreator = - username && users?.userList.find(user => user.username === username); - if (error instanceof UnauthorizedError) { return ; } @@ -149,9 +135,6 @@ export const SplunkOnCallCard = ({ entity }: Props) => { if (!team) { return ; } - if (!username || !incidentCreator) { - return ; - } if (!eventsRestEndpoint) { return ; @@ -163,15 +146,12 @@ export const SplunkOnCallCard = ({ entity }: Props) => { {users?.usersHashMap && team && ( )} - {incidentCreator && ( - - )} + ); }; @@ -191,15 +171,22 @@ export const SplunkOnCallCard = ({ entity }: Props) => { icon: , }; + const serviceLink = { + label: 'Portal', + href: 'https://portal.victorops.com/', + icon: , + }; + return ( Team: {team}, - username && ( - - ), + , ]} /> diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx index a43dc548dd..2872c80ba0 100644 --- a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx +++ b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx @@ -36,12 +36,10 @@ import { useApi, alertApiRef } from '@backstage/core'; import { useAsyncFn } from 'react-use'; import { splunkOnCallApiRef } from '../../api'; import { Alert } from '@material-ui/lab'; -import { User } from '../types'; import { TriggerAlarmRequest } from '../../api/types'; type Props = { team: string; - incidentCreator: User; showDialog: boolean; handleDialog: () => void; onIncidentCreated: () => void; @@ -58,8 +56,17 @@ const useStyles = makeStyles((theme: Theme) => }, formControl: { margin: theme.spacing(1), + display: 'flex', + flexDirection: 'row', + alignItems: 'center', minWidth: `calc(100% - ${theme.spacing(2)}px)`, }, + formHeader: { + width: '50%', + }, + incidentType: { + width: '90%', + }, targets: { display: 'flex', flexDirection: 'column', @@ -70,7 +77,6 @@ const useStyles = makeStyles((theme: Theme) => export const TriggerDialog = ({ team, - incidentCreator, showDialog, handleDialog, onIncidentCreated: onIncidentCreated, @@ -142,10 +148,7 @@ export const TriggerDialog = ({ This action will trigger an incident - Created by:{' '} - - {incidentCreator?.firstName} {incidentCreator?.lastName} - + Created by: {`{ REST } Endpoint`} @@ -163,22 +166,35 @@ export const TriggerDialog = ({ align="justify" > Please describe the problem you want to report. Be as descriptive as - possible. Your signed in user and a reference to the current page will - automatically be amended to the alarm so that the receiver can reach - out to you if necessary. + possible.
+ Note that only the Incident type, Incident display name{' '} + and the Incident message fields are required.
- Incident type - +
+ Incident type + +
+
-
+ + + ))} + ); + +export const Styling = () => { + const classes = useStyles(); + return ( + + + Both the grid and the header can be styled, using the{' '} + classes property. This lets + you for example tweak the column sizes and the background of the header. + + + {[...Array(10).keys()].map(index => ( + + + + + + {text + .split(' ') + .slice(0, 5 + Math.floor(Math.random() * 30)) + .join(' ')} + + + + + + ))} + + + ); +}; diff --git a/packages/core/src/layout/ItemCard/ItemCard.tsx b/packages/core/src/layout/ItemCard/ItemCard.tsx index 9e16def5d3..8c26e6a9f1 100644 --- a/packages/core/src/layout/ItemCard/ItemCard.tsx +++ b/packages/core/src/layout/ItemCard/ItemCard.tsx @@ -13,33 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Button, Card, Chip, makeStyles, Typography } from '@material-ui/core'; -import clsx from 'clsx'; +import { + Box, + Card, + CardActions, + CardContent, + CardMedia, + Chip, +} from '@material-ui/core'; import React, { ReactNode } from 'react'; -import { Link } from '../../components'; - -const useStyles = makeStyles(theme => ({ - header: { - color: theme.palette.common.white, - padding: theme.spacing(2, 2, 6), - backgroundImage: 'linear-gradient(-137deg, #4BB8A5 0%, #187656 100%)', - }, - content: { - padding: theme.spacing(2), - }, - description: { - height: 175, - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - withTags: { - height: 'calc(175px - 32px - 8px)', - }, - footer: { - display: 'flex', - flexDirection: 'row-reverse', - }, -})); +import { Button } from '../../components'; +import { ItemCardHeader } from './ItemCardHeader'; type ItemCardProps = { description?: string; @@ -53,6 +37,31 @@ type ItemCardProps = { href?: string; }; +/** + * This card type has been deprecated. Instead use plain MUI Card and helpers + * where appropriate. + * + * + * + * + * + * @deprecated Use plain MUI and composable helpers instead. + * @see https://material-ui.com/components/cards/ + */ export const ItemCard = ({ description, tags, @@ -63,43 +72,33 @@ export const ItemCard = ({ onClick, href, }: ItemCardProps) => { - const classes = useStyles(); - return ( -
- {(subtitle || type) && ( - {subtitle ?? type} + + + + + {tags?.length ? ( + + {tags.map((tag, i) => ( + + ))} + + ) : null} + {description} + + + {!href && ( + )} - {title} -
-
- {tags?.map((tag, i) => ( - - ))} - 0 && classes.withTags, - )} - > - {description} - -
- {!href && ( - - )} - {href && ( - - )} -
-
+ {href && ( + + )} +
); }; diff --git a/packages/core/src/layout/ItemCard/ItemCardGrid.test.tsx b/packages/core/src/layout/ItemCard/ItemCardGrid.test.tsx new file mode 100644 index 0000000000..51ef8c544f --- /dev/null +++ b/packages/core/src/layout/ItemCard/ItemCardGrid.test.tsx @@ -0,0 +1,50 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { Card } from '@material-ui/core'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { ItemCardGrid } from './ItemCardGrid'; + +describe('', () => { + it('renders default without exploding', async () => { + await renderInTestApp( + + Hello! + , + ); + expect(screen.getByRole('grid')).toBeInTheDocument(); + expect(screen.getByText('Hello!')).toBeInTheDocument(); + }); + + it('renders custom styles', async () => { + await renderInTestApp( + <> + + Hello! + + + Goodbye! + + , + ); + expect(screen.getAllByRole('grid')[0]).toHaveStyle({ + gridTemplateColumns: 'repeat(auto-fill, minmax(22em, 1fr))', + }); + expect(screen.getAllByRole('grid')[1]).toHaveClass('my-css-class'); + }); +}); diff --git a/packages/core/src/layout/ItemCard/ItemCardGrid.tsx b/packages/core/src/layout/ItemCard/ItemCardGrid.tsx new file mode 100644 index 0000000000..551c3c67d5 --- /dev/null +++ b/packages/core/src/layout/ItemCard/ItemCardGrid.tsx @@ -0,0 +1,62 @@ +/* + * 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 { createStyles, makeStyles, Theme, WithStyles } from '@material-ui/core'; +import React from 'react'; + +const styles = (theme: Theme) => + createStyles({ + root: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fill, minmax(22em, 1fr))', + gridAutoRows: '1fr', + gridGap: theme.spacing(2), + }, + }); + +const useStyles = makeStyles(styles); + +export type ItemCardGridProps = Partial> & { + /** + * The Card items of the grid. + */ + children?: React.ReactNode; +}; + +/** + * A default grid to use when arranging "item cards" - cards that let users + * select among several options. + * + * The immediate children are expected to be MUI Card components. + * + * Styles for the grid can be overridden using the `classes` prop, e.g.: + * + * + * + * + * + * This can be useful for e.g. overriding gridTemplateColumns to adapt the + * minimum size of the cells to fit the content better. + */ +export const ItemCardGrid = (props: ItemCardGridProps) => { + const { children, ...otherProps } = props; + const classes = useStyles(otherProps); + return ( +
+ {children} +
+ ); +}; diff --git a/packages/core/src/layout/ItemCard/ItemCardHeader.test.tsx b/packages/core/src/layout/ItemCard/ItemCardHeader.test.tsx new file mode 100644 index 0000000000..ec9af6436e --- /dev/null +++ b/packages/core/src/layout/ItemCard/ItemCardHeader.test.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 { renderInTestApp } from '@backstage/test-utils'; +import { Card, CardMedia } from '@material-ui/core'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { ItemCardHeader } from './ItemCardHeader'; + +describe('', () => { + it('renders default without exploding', async () => { + await renderInTestApp( + + + + + , + ); + expect(screen.getByText('My Title')).toBeInTheDocument(); + expect(screen.getByText('My Subtitle')).toBeInTheDocument(); + }); + + it('renders custom children', async () => { + await renderInTestApp( + + + My Custom Text + + , + ); + expect(screen.getByText('My Title')).toBeInTheDocument(); + expect(screen.getByText('My Custom Text')).toBeInTheDocument(); + }); + + it('renders custom styles', async () => { + await renderInTestApp( + + + + My Custom Text + + + , + ); + expect(screen.getByText('My Custom Text')).toHaveClass('my-css-class'); + }); +}); diff --git a/packages/core/src/layout/ItemCard/ItemCardHeader.tsx b/packages/core/src/layout/ItemCard/ItemCardHeader.tsx new file mode 100644 index 0000000000..17c4a19e58 --- /dev/null +++ b/packages/core/src/layout/ItemCard/ItemCardHeader.tsx @@ -0,0 +1,85 @@ +/* + * 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 { + createStyles, + makeStyles, + Theme, + Typography, + WithStyles, +} from '@material-ui/core'; +import React from 'react'; + +const styles = (theme: Theme) => + createStyles({ + root: { + color: theme.palette.common.white, + padding: theme.spacing(2, 2, 3), + backgroundImage: 'linear-gradient(-137deg, #4BB8A5 0%, #187656 100%)', + backgroundPosition: 0, + backgroundSize: 'inherit', + }, + }); + +const useStyles = makeStyles(styles); + +export type ItemCardHeaderProps = Partial> & { + /** + * A large title to show in the header, providing the main heading. + * + * Use this if you want to have the default styling and placement of a title. + */ + title?: React.ReactNode; + /** + * A slightly smaller title to show in the header, providing additional + * details. + * + * Use this if you want to have the default styling and placement of a + * subtitle. + */ + subtitle?: React.ReactNode; + /** + * Custom children to draw in the header. + * + * If the title and/or subtitle were specified, the children are drawn below + * those. + */ + children?: React.ReactNode; +}; + +/** + * A simple card header, rendering a default look for "item cards" - cards that + * are arranged in a grid for users to select among several options. + * + * This component expects to be placed within a MUI . + * + * Styles for the header can be overridden using the `classes` prop, e.g.: + * + * + * + * + */ +export const ItemCardHeader = (props: ItemCardHeaderProps) => { + const { title, subtitle, children } = props; + const classes = useStyles(props); + return ( +
+ {subtitle && {subtitle}} + {title && {title}} + {children} +
+ ); +}; diff --git a/packages/core/src/layout/ItemCard/index.ts b/packages/core/src/layout/ItemCard/index.ts index b38dd2acc2..da2c1dd546 100644 --- a/packages/core/src/layout/ItemCard/index.ts +++ b/packages/core/src/layout/ItemCard/index.ts @@ -15,3 +15,7 @@ */ export { ItemCard } from './ItemCard'; +export { ItemCardGrid } from './ItemCardGrid'; +export type { ItemCardGridProps } from './ItemCardGrid'; +export { ItemCardHeader } from './ItemCardHeader'; +export type { ItemCardHeaderProps } from './ItemCardHeader'; diff --git a/packages/theme/src/baseTheme.ts b/packages/theme/src/baseTheme.ts index 37b8e3b47f..e7bb7e58b6 100644 --- a/packages/theme/src/baseTheme.ts +++ b/packages/theme/src/baseTheme.ts @@ -234,12 +234,31 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides { margin: `0 ${theme.spacing(0.5)}px 0 -${theme.spacing(0.5)}px`, }, }, + MuiCard: { + root: { + // When cards have a forced size, such as when they are arranged in a + // CSS grid, the content needs to flex such that the actions (buttons + // etc) end up at the bottom of the card instead of just below the body + // contents. + display: 'flex', + flexDirection: 'column', + }, + }, MuiCardHeader: { root: { // Reduce padding between header and content paddingBottom: 0, }, }, + MuiCardContent: { + root: { + // When cards have a forced size, such as when they are arranged in a + // CSS grid, the content needs to flex such that the actions (buttons + // etc) end up at the bottom of the card instead of just below the body + // contents. + flexGrow: 1, + }, + }, MuiCardActions: { root: { // We default to putting the card actions at the end diff --git a/plugins/explore/src/components/DomainCard/DomainCard.tsx b/plugins/explore/src/components/DomainCard/DomainCard.tsx index 67bee13696..ee06fd39fd 100644 --- a/plugins/explore/src/components/DomainCard/DomainCard.tsx +++ b/plugins/explore/src/components/DomainCard/DomainCard.tsx @@ -14,12 +14,20 @@ * limitations under the License. */ import { DomainEntity, RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { ItemCard, useRouteRef } from '@backstage/core'; +import { Button, ItemCardHeader, useRouteRef } from '@backstage/core'; import { EntityRefLinks, entityRouteParams, getEntityRelations, } from '@backstage/plugin-catalog-react'; +import { + Box, + Card, + CardActions, + CardContent, + CardMedia, + Chip, +} from '@material-ui/core'; import React from 'react'; import { catalogEntityRouteRef } from '../../routes'; @@ -28,23 +36,39 @@ type DomainCardProps = { }; export const DomainCard = ({ entity }: DomainCardProps) => { - const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); const catalogEntityRoute = useRouteRef(catalogEntityRouteRef); - return ( - - } - label="Explore" - href={catalogEntityRoute(entityRouteParams(entity))} + const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); + const url = catalogEntityRoute(entityRouteParams(entity)); + + const owner = ( + ); + + return ( + + + + + + {entity.metadata.tags?.length ? ( + + {entity.metadata.tags.map(tag => ( + + ))} + + ) : null} + {entity.metadata.description} + + + + + + ); }; diff --git a/plugins/explore/src/components/DomainCard/DomainCardGrid.test.tsx b/plugins/explore/src/components/DomainCard/DomainCardGrid.test.tsx deleted file mode 100644 index ee06a50427..0000000000 --- a/plugins/explore/src/components/DomainCard/DomainCardGrid.test.tsx +++ /dev/null @@ -1,59 +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 { DomainEntity } from '@backstage/catalog-model'; -import { renderInTestApp } from '@backstage/test-utils'; -import React from 'react'; -import { catalogEntityRouteRef } from '../../routes'; -import { DomainCardGrid } from './DomainCardGrid'; - -describe('', () => { - it('renders a grid of domain cards', async () => { - const entities: DomainEntity[] = [ - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Domain', - metadata: { - name: 'playback', - }, - spec: { - owner: 'guest', - }, - }, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Domain', - metadata: { - name: 'artists', - }, - spec: { - owner: 'guest', - }, - }, - ]; - const { getByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': catalogEntityRouteRef, - }, - }, - ); - - expect(getByText('artists')).toBeInTheDocument(); - expect(getByText('playback')).toBeInTheDocument(); - }); -}); diff --git a/plugins/explore/src/components/DomainCard/DomainCardGrid.tsx b/plugins/explore/src/components/DomainCard/DomainCardGrid.tsx deleted file mode 100644 index b4a13aa39f..0000000000 --- a/plugins/explore/src/components/DomainCard/DomainCardGrid.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 { DomainEntity } from '@backstage/catalog-model'; -import { Grid } from '@material-ui/core'; -import React from 'react'; -import { DomainCard } from '.'; - -type DomainCardGridProps = { - entities: DomainEntity[]; -}; - -export const DomainCardGrid = ({ entities }: DomainCardGridProps) => ( - - {entities.map((e, i) => ( - - - - ))} - -); diff --git a/plugins/explore/src/components/DomainCard/index.ts b/plugins/explore/src/components/DomainCard/index.ts index 6acf10800a..3511ff023d 100644 --- a/plugins/explore/src/components/DomainCard/index.ts +++ b/plugins/explore/src/components/DomainCard/index.ts @@ -14,4 +14,3 @@ * limitations under the License. */ export { DomainCard } from './DomainCard'; -export { DomainCardGrid } from './DomainCardGrid'; diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx index 182adf70ea..a12811e486 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx @@ -18,6 +18,7 @@ import { Content, ContentHeader, EmptyState, + ItemCardGrid, Progress, SupportButton, useApi, @@ -27,47 +28,64 @@ import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Button } from '@material-ui/core'; import React from 'react'; import { useAsync } from 'react-use'; -import { DomainCardGrid } from '../DomainCard'; +import { DomainCard } from '../DomainCard'; -export const DomainExplorerContent = () => { +const Body = () => { const catalogApi = useApi(catalogApiRef); const { value: entities, loading, error } = useAsync(async () => { const response = await catalogApi.getEntities({ filter: { kind: 'domain' }, }); - return response.items as DomainEntity[]; }, [catalogApi]); + if (loading) { + return ; + } + + if (error) { + return ( + + {error.message} + + ); + } + + if (!entities?.length) { + return ( + + Read more + + } + /> + ); + } + + return ( + + {entities.map((entity, index) => ( + + ))} + + ); +}; + +export const DomainExplorerContent = () => { return ( Discover the domains in your ecosystem. - - {loading && } - {error && ( - - {error.message} - - )} - {!loading && !error && (!entities || entities.length === 0) && ( - - Read more - - } - /> - )} - {!loading && entities && } + ); }; diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index ebce35a91f..8e8ba15ddc 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -14,14 +14,13 @@ * limitations under the License. */ -import React, { useEffect, useMemo, useState } from 'react'; -import { Link as RouterLink } from 'react-router-dom'; import { EntityMeta, TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { configApiRef, Content, ContentHeader, Header, + ItemCardGrid, Lifecycle, Page, Progress, @@ -30,14 +29,16 @@ import { WarningPanel, } from '@backstage/core'; import { useStarredEntities } from '@backstage/plugin-catalog-react'; -import { Box, Button, Link, makeStyles, Typography } from '@material-ui/core'; +import { Button, Link, makeStyles, Typography } from '@material-ui/core'; import StarIcon from '@material-ui/icons/Star'; +import React, { useEffect, useMemo, useState } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter'; -import { TemplateCard, TemplateCardProps } from '../TemplateCard'; import { ResultsFilter } from '../ResultsFilter/ResultsFilter'; import { ScaffolderFilter } from '../ScaffolderFilter'; import { ButtonGroup } from '../ScaffolderFilter/ScaffolderFilter'; import SearchToolbar from '../SearchToolbar/SearchToolbar'; +import { TemplateCard, TemplateCardProps } from '../TemplateCard'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -46,12 +47,6 @@ const useStyles = makeStyles(theme => ({ gridTemplateColumns: '250px 1fr', gridColumnGap: theme.spacing(2), }, - templateGrid: { - display: 'grid', - gridTemplateColumns: 'repeat(auto-fill, minmax(22em, 1fr))', - gridAutoRows: '1fr', - gridGap: theme.spacing(2), - }, })); const getTemplateCardProps = ( @@ -183,13 +178,13 @@ export const ScaffolderPageContents = () => { )} - + {matchingEntities && matchingEntities?.length > 0 && matchingEntities.map(template => ( ))} - +
diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index 26d5c79cc7..25f89f20ab 100644 --- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -13,29 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Button, useRouteRef } from '@backstage/core'; +import { Button, ItemCardHeader, useRouteRef } from '@backstage/core'; import { BackstageTheme, pageTheme } from '@backstage/theme'; import { + Box, Card, CardActions, CardContent, CardMedia, Chip, makeStyles, - Typography, useTheme, } from '@material-ui/core'; import React from 'react'; import { generatePath } from 'react-router'; import { rootRouteRef } from '../../routes'; -const useStyles = makeStyles(theme => ({ - header: { - color: theme.palette.common.white, - padding: theme.spacing(2, 2, 3), - backgroundImage: (props: { backgroundImage: string }) => - props.backgroundImage, - backgroundPosition: 0, +const useStyles = makeStyles({ + title: { + backgroundImage: ({ backgroundImage }: any) => backgroundImage, }, description: { overflow: 'hidden', @@ -44,14 +40,7 @@ const useStyles = makeStyles(theme => ({ '-webkit-line-clamp': 10, '-webkit-box-orient': 'vertical', }, - card: { - display: 'flex', - flexDirection: 'column', - }, - cardContent: { - flexGrow: 1, - }, -})); +}); export type TemplateCardProps = { description: string; @@ -79,18 +68,21 @@ export const TemplateCard = ({ }); return ( - - - {type} - {title} + + + - - {tags?.map(tag => ( - - ))} - - {description} - + + + {tags?.map(tag => ( + + ))} + + {description} + + + ))} + ); From b17819246cb0a75fcf4a7fe663ef5faa872eba25 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Thu, 4 Mar 2021 11:58:20 -0700 Subject: [PATCH 135/147] Add note about monorepo Signed-off-by: Tim Hansen --- docs/getting-started/configure-app-with-plugins.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/getting-started/configure-app-with-plugins.md b/docs/getting-started/configure-app-with-plugins.md index af7fdbb4cb..cf7806a5d2 100644 --- a/docs/getting-started/configure-app-with-plugins.md +++ b/docs/getting-started/configure-app-with-plugins.md @@ -22,7 +22,12 @@ plugin in this example. yarn workspace app add @backstage/plugin-circleci ``` -2. Add the plugin itself: +Note the plugin is added to the `app` package, rather than the root +package.json. Backstage Apps are set up as monorepos with +[yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/). Since +CircleCI is a frontend UI plugin, it goes in `app` rather than `backend`. + +2. Add the plugin itself to the App: ```js // packages/app/src/plugins.ts From 82f329dda8b7678a51cabf1524d10097c44eed2d Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 4 Mar 2021 14:22:25 -0500 Subject: [PATCH 136/147] Clarify file type Signed-off-by: Adam Harvey --- packages/techdocs-common/src/stages/generate/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/packages/techdocs-common/src/stages/generate/helpers.ts index a88ed13231..2035eb8da9 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.ts @@ -252,7 +252,7 @@ export const patchMkdocsYmlPreBuild = async ( mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); } catch (error) { logger.warn( - `Could not read file ${mkdocsYmlPath} before running the generator. ${error.message}`, + `Could not read MkDocs YAML config file ${mkdocsYmlPath} before running the generator: ${error.message}`, ); return; } From f95804c311205c10ab2b7480f9b1874d709a2145 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 4 Mar 2021 14:22:43 -0500 Subject: [PATCH 137/147] Change log msgs to debug Signed-off-by: Adam Harvey --- packages/techdocs-common/src/helpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/techdocs-common/src/helpers.ts b/packages/techdocs-common/src/helpers.ts index ec58bb7b40..d02297f2fd 100644 --- a/packages/techdocs-common/src/helpers.ts +++ b/packages/techdocs-common/src/helpers.ts @@ -216,12 +216,12 @@ export const getDocFilesFromRepository = async ( entity, ); - opts?.logger?.info(`Reading files from ${target}`); + opts?.logger?.debug(`Reading files from ${target}`); // readTree will throw NotModifiedError if etag has not changed. const readTreeResponse = await reader.readTree(target, { etag: opts?.etag }); const preparedDir = await readTreeResponse.dir(); - opts?.logger?.info(`Tree downloaded and stored at ${preparedDir}`); + opts?.logger?.debug(`Tree downloaded and stored at ${preparedDir}`); return { preparedDir, From 26e23b2c7d46159c8849e6c6ff97792bd07472e2 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 4 Mar 2021 14:23:01 -0500 Subject: [PATCH 138/147] Refactor logging Signed-off-by: Adam Harvey --- .../src/DocsBuilder/builder.ts | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index 51f4d212ba..38cc7ca562 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { NotModifiedError } from '@backstage/backend-common'; -import { Entity } from '@backstage/catalog-model'; +import { Entity, serializeEntityRef } from '@backstage/catalog-model'; import { GeneratorBase, GeneratorBuilder, @@ -31,12 +31,6 @@ import path from 'path'; import { Logger } from 'winston'; import { BuildMetadataStorage } from '.'; -const getEntityId = (entity: Entity) => { - return `${entity.kind}:${entity.metadata.namespace ?? ''}:${ - entity.metadata.name - }`; -}; - type DocsBuilderArguments = { preparers: PreparerBuilder; generators: GeneratorBuilder; @@ -78,9 +72,15 @@ export class DocsBuilder { } /** - * Prepare and cache check + * Prepare (and cache check) */ + this.logger.info( + `Step 1 of 3: Preparing docs for entity ${serializeEntityRef( + this.entity, + )}`, + ); + // Use the in-memory storage for setting and getting etag for this entity. const buildMetadataStorage = new BuildMetadataStorage( this.entity.metadata.uid, @@ -92,6 +92,7 @@ export class DocsBuilder { // make sure to limit checking for cache invalidation to once per minute or so. let preparedDir: string; let etag: string; + try { const preparerResponse = await this.preparer.prepare(this.entity, { etag: buildMetadataStorage.getEtag(), @@ -102,21 +103,32 @@ export class DocsBuilder { } catch (err) { if (err instanceof NotModifiedError) { // No need to prepare anymore since cache is valid. + this.logger.debug( + `Docs for ${serializeEntityRef( + this.entity, + )} are unmodified. Using cache, skipping generate and prepare`, + ); return; } throw new Error(err.message); } this.logger.info( - `TechDocs prepare step completed for entity ${getEntityId(this.entity)}.`, + `Prepare step completed for entity ${serializeEntityRef( + this.entity, + )}, stored at ${preparedDir}`, ); - this.logger.debug(`Prepared files temporarily stored at ${preparedDir}`); /** * Generate */ - this.logger.info(`Running generator on entity ${getEntityId(this.entity)}`); + this.logger.info( + `Step 2 of 3: Generating docs for entity ${serializeEntityRef( + this.entity, + )}`, + ); + // Create a temporary directory to store the generated files in. const tmpdirPath = os.tmpdir(); // Fixes a problem with macOS returning a path that is a symlink @@ -133,13 +145,12 @@ export class DocsBuilder { etag, }); - this.logger.debug(`Generated files temporarily stored at ${outputDir}`); // Remove Prepared directory since it is no longer needed. // Caveat: Can not remove prepared directory in case of git preparer since the // local git repository is used to get etag on subsequent requests. if (this.preparer instanceof UrlPreparer) { this.logger.debug( - `Removing prepared directory ${preparedDir} since the site has been generated.`, + `Removing prepared directory ${preparedDir} since the site has been generated`, ); try { // Not a blocker hence no need to await this. @@ -153,17 +164,23 @@ export class DocsBuilder { * Publish */ - this.logger.info(`Running publisher on entity ${getEntityId(this.entity)}`); + this.logger.info( + `Step 3 of 3: Publishing docs for entity ${serializeEntityRef( + this.entity, + )}`, + ); + await this.publisher.publish({ entity: this.entity, directory: outputDir, }); - this.logger.debug( - `Removing generated directory ${outputDir} since the site has been published`, - ); + try { // Not a blocker hence no need to await this. fs.remove(outputDir); + this.logger.debug( + `Removing generated directory ${outputDir} since the site has been published`, + ); } catch (error) { this.logger.debug(`Error removing generated directory ${error.message}`); } From 6326963c98b97a7f9223c089c9dd49726d6efa48 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 4 Mar 2021 14:24:01 -0500 Subject: [PATCH 139/147] Add changeset Signed-off-by: Adam Harvey --- .changeset/old-guests-add.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/old-guests-add.md diff --git a/.changeset/old-guests-add.md b/.changeset/old-guests-add.md new file mode 100644 index 0000000000..e4f991a1b6 --- /dev/null +++ b/.changeset/old-guests-add.md @@ -0,0 +1,6 @@ +--- +'@backstage/techdocs-common': patch +'@backstage/plugin-techdocs-backend': patch +--- + +Refactor log messaging to improve clarity From a501128dbda8215718ad29409e4ea48cc1c83cb2 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 4 Mar 2021 14:33:35 -0500 Subject: [PATCH 140/147] Update changeset to trigger CODEOWNERS Signed-off-by: Adam Harvey --- .changeset/{old-guests-add.md => techdocs-old-guests-add.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changeset/{old-guests-add.md => techdocs-old-guests-add.md} (100%) diff --git a/.changeset/old-guests-add.md b/.changeset/techdocs-old-guests-add.md similarity index 100% rename from .changeset/old-guests-add.md rename to .changeset/techdocs-old-guests-add.md From bc46435f518f5bf24772b51c458f14f6a2f4afb6 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 4 Mar 2021 14:35:54 -0500 Subject: [PATCH 141/147] Update changeset name for CODEOWNERS Signed-off-by: Adam Harvey --- .changeset/{metal-trees-push.md => techdocs-metal-trees-push.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changeset/{metal-trees-push.md => techdocs-metal-trees-push.md} (100%) diff --git a/.changeset/metal-trees-push.md b/.changeset/techdocs-metal-trees-push.md similarity index 100% rename from .changeset/metal-trees-push.md rename to .changeset/techdocs-metal-trees-push.md From 02d78290a5ba9d49f5e16944bb3923e301a7fd00 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 4 Mar 2021 15:58:54 -0500 Subject: [PATCH 142/147] Enhance example documented-component Signed-off-by: Adam Harvey --- .changeset/techdocs-kind-seals-fix.md | 5 ++++ .../docs/code/code-sample.md | 22 ++++++++++++++ .../docs/images/backstage-logo-cncf.svg | 1 + .../documented-component/docs/index.md | 29 ++++++++++++++++++- .../documented-component/docs/sub-page.md | 10 +++++++ .../examples/documented-component/mkdocs.yml | 5 ++-- 6 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 .changeset/techdocs-kind-seals-fix.md create mode 100644 plugins/techdocs-backend/examples/documented-component/docs/code/code-sample.md create mode 100644 plugins/techdocs-backend/examples/documented-component/docs/images/backstage-logo-cncf.svg diff --git a/.changeset/techdocs-kind-seals-fix.md b/.changeset/techdocs-kind-seals-fix.md new file mode 100644 index 0000000000..b0ac18c744 --- /dev/null +++ b/.changeset/techdocs-kind-seals-fix.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Enhanced the example documented-component to better demonstrate TechDocs features diff --git a/plugins/techdocs-backend/examples/documented-component/docs/code/code-sample.md b/plugins/techdocs-backend/examples/documented-component/docs/code/code-sample.md new file mode 100644 index 0000000000..ed430edb15 --- /dev/null +++ b/plugins/techdocs-backend/examples/documented-component/docs/code/code-sample.md @@ -0,0 +1,22 @@ +# Sample Code + +This page provides some sample code which may be used in your example component. + +This code uses TypeScript, and the Markdown code fence to wrap the code. + +```tsx +const DefaultEntityPage = ({ entity }: { entity: Entity }) => ( + + } + /> + } + /> + +); +``` diff --git a/plugins/techdocs-backend/examples/documented-component/docs/images/backstage-logo-cncf.svg b/plugins/techdocs-backend/examples/documented-component/docs/images/backstage-logo-cncf.svg new file mode 100644 index 0000000000..b5ff591d1b --- /dev/null +++ b/plugins/techdocs-backend/examples/documented-component/docs/images/backstage-logo-cncf.svg @@ -0,0 +1 @@ +05 Logo_Black \ No newline at end of file diff --git a/plugins/techdocs-backend/examples/documented-component/docs/index.md b/plugins/techdocs-backend/examples/documented-component/docs/index.md index bf0895710f..c22dd18208 100644 --- a/plugins/techdocs-backend/examples/documented-component/docs/index.md +++ b/plugins/techdocs-backend/examples/documented-component/docs/index.md @@ -1,3 +1,30 @@ -# example docs +# Welcome! This is a basic example of documentation. + +Here is a bulleted list: + +- Item one +- Item two +- Item Three + +Check out the [Markdown Guide](https://www.markdownguide.org/) to learn more about how to simply create documentation. + +You can also learn more about how to configure and setup this documentation in Backstage, +[read up on the TechDocs Overview](https://backstage.io/docs/features/techdocs/techdocs-overview). + +## Image Example + +This documentation is powered by Backstage's TechDocs feature: + +![Backstage Logo](images/backstage-logo-cncf.svg) + +## Table Example + +While this documentation isn't comprehensive, in the future it should cover the following topics outlined in this example table: + +| Topic | Description | +| ------- | ------------------------------------------------------------ | +| Topic 1 | An introductory topic to help you learn about the topic. | +| Topic 2 | A more detailed topic that explains more information. | +| Topic 3 | A final topic that provides conclusions and lessons learned. | diff --git a/plugins/techdocs-backend/examples/documented-component/docs/sub-page.md b/plugins/techdocs-backend/examples/documented-component/docs/sub-page.md index bbd98558e0..62de70a273 100644 --- a/plugins/techdocs-backend/examples/documented-component/docs/sub-page.md +++ b/plugins/techdocs-backend/examples/documented-component/docs/sub-page.md @@ -1 +1,11 @@ # Another page in our documentation + +This sub-page can be used to elaborate on a specific part of the component. + +## Details + +It is linked off the main page, and due to it's inclusion in the `mkdocs.yml` file, becomes part of the auto-generated site navigation. + +## MkDocs + +Visit https://www.mkdocs.org for more information about the MkDocs tool. diff --git a/plugins/techdocs-backend/examples/documented-component/mkdocs.yml b/plugins/techdocs-backend/examples/documented-component/mkdocs.yml index 3b48e8edff..4c7d018690 100644 --- a/plugins/techdocs-backend/examples/documented-component/mkdocs.yml +++ b/plugins/techdocs-backend/examples/documented-component/mkdocs.yml @@ -1,8 +1,9 @@ -site_name: 'example-docs' +site_name: 'Example Documentation' nav: - Home: index.md - - SubPage: sub-page.md + - Subpage: sub-page.md + - 'Code Sample': code/code-sample.md plugins: - techdocs-core From c149b14009bc836af92f08b8bd11582223390d66 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 4 Mar 2021 18:51:34 -0500 Subject: [PATCH 143/147] Fix typo Signed-off-by: Adam Harvey --- .../examples/documented-component/docs/sub-page.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/techdocs-backend/examples/documented-component/docs/sub-page.md b/plugins/techdocs-backend/examples/documented-component/docs/sub-page.md index 62de70a273..8e1870c7b4 100644 --- a/plugins/techdocs-backend/examples/documented-component/docs/sub-page.md +++ b/plugins/techdocs-backend/examples/documented-component/docs/sub-page.md @@ -4,7 +4,7 @@ This sub-page can be used to elaborate on a specific part of the component. ## Details -It is linked off the main page, and due to it's inclusion in the `mkdocs.yml` file, becomes part of the auto-generated site navigation. +It is linked off the main page, and due to its inclusion in the `mkdocs.yml` file, becomes part of the auto-generated site navigation. ## MkDocs From 2daa65a18b4c7800e2d96e114e1cfda916df9d58 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 4 Mar 2021 18:51:57 -0500 Subject: [PATCH 144/147] Change wording Signed-off-by: Adam Harvey --- .../examples/documented-component/docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/techdocs-backend/examples/documented-component/docs/index.md b/plugins/techdocs-backend/examples/documented-component/docs/index.md index c22dd18208..14d09ff2fc 100644 --- a/plugins/techdocs-backend/examples/documented-component/docs/index.md +++ b/plugins/techdocs-backend/examples/documented-component/docs/index.md @@ -25,6 +25,6 @@ While this documentation isn't comprehensive, in the future it should cover the | Topic | Description | | ------- | ------------------------------------------------------------ | -| Topic 1 | An introductory topic to help you learn about the topic. | +| Topic 1 | An introductory topic to help you learn about the component. | | Topic 2 | A more detailed topic that explains more information. | | Topic 3 | A final topic that provides conclusions and lessons learned. | From a9d7c123fbc093c5d9f6fb9042010d0148892a12 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 4 Mar 2021 18:53:39 -0500 Subject: [PATCH 145/147] Wording tweaks Signed-off-by: Adam Harvey --- .../examples/documented-component/docs/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/techdocs-backend/examples/documented-component/docs/index.md b/plugins/techdocs-backend/examples/documented-component/docs/index.md index 14d09ff2fc..5a9c23d9f6 100644 --- a/plugins/techdocs-backend/examples/documented-component/docs/index.md +++ b/plugins/techdocs-backend/examples/documented-component/docs/index.md @@ -6,12 +6,12 @@ Here is a bulleted list: - Item one - Item two -- Item Three +- Item three Check out the [Markdown Guide](https://www.markdownguide.org/) to learn more about how to simply create documentation. -You can also learn more about how to configure and setup this documentation in Backstage, -[read up on the TechDocs Overview](https://backstage.io/docs/features/techdocs/techdocs-overview). +You can also learn more about how to configure and setup this documentation in Backstage by +[reading up on the TechDocs Overview](https://backstage.io/docs/features/techdocs/techdocs-overview). ## Image Example From b876bc6d9afdf9047d629c7e6815d2da83ef0054 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Mar 2021 04:33:20 +0000 Subject: [PATCH 146/147] chore(deps-dev): bump @storybook/addons from 6.1.20 to 6.1.21 Bumps [@storybook/addons](https://github.com/storybookjs/storybook/tree/HEAD/lib/addons) from 6.1.20 to 6.1.21. - [Release notes](https://github.com/storybookjs/storybook/releases) - [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md) - [Commits](https://github.com/storybookjs/storybook/commits/v6.1.21/lib/addons) Signed-off-by: dependabot[bot] --- yarn.lock | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 24cd8d5b51..c258de0c0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4713,7 +4713,7 @@ global "^4.3.2" regenerator-runtime "^0.13.7" -"@storybook/addons@6.1.20", "@storybook/addons@^6.1.11": +"@storybook/addons@6.1.20": version "6.1.20" resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.1.20.tgz#da01dabd6692919b719fcb30519d53ea80887097" integrity sha512-kIhXYgF+ARNpYxO3qhz8yThDvKpaq+HDst8odPU9sCNEI66PSH6hrILhTmnffNnqdtY3LnKkU9rGVfZn+3TOTA== @@ -4728,6 +4728,21 @@ global "^4.3.2" regenerator-runtime "^0.13.7" +"@storybook/addons@^6.1.11": + version "6.1.21" + resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.1.21.tgz#94bb66fc51d1dfee80d0fe84f5b83c10045651b5" + integrity sha512-xo5TGu9EZVCqgh3D1veVnfuGzyKDWWsvOMo18phVqRxj21G3/+hScVyfIYwNTv7Ys5/Ahp9JxJUMXL3V3ny+tw== + dependencies: + "@storybook/api" "6.1.21" + "@storybook/channels" "6.1.21" + "@storybook/client-logger" "6.1.21" + "@storybook/core-events" "6.1.21" + "@storybook/router" "6.1.21" + "@storybook/theming" "6.1.21" + core-js "^3.0.1" + global "^4.3.2" + regenerator-runtime "^0.13.7" + "@storybook/api@6.1.11": version "6.1.11" resolved "https://registry.npmjs.org/@storybook/api/-/api-6.1.11.tgz#1e0b798203df823ac21184386258cf8b5f17f440" @@ -4828,6 +4843,31 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/api@6.1.21": + version "6.1.21" + resolved "https://registry.npmjs.org/@storybook/api/-/api-6.1.21.tgz#be753ca8d3602efe4a11783c81c689463bee0825" + integrity sha512-QjZk70VSXMw/wPPoWdMp5Bl9VmkfmGhIz8PALrFLLEZHjzptpfZE2qkGEEJHG0NAksFUv6NxGki2/632dzR7Ug== + dependencies: + "@reach/router" "^1.3.3" + "@storybook/channels" "6.1.21" + "@storybook/client-logger" "6.1.21" + "@storybook/core-events" "6.1.21" + "@storybook/csf" "0.0.1" + "@storybook/router" "6.1.21" + "@storybook/semver" "^7.3.2" + "@storybook/theming" "6.1.21" + "@types/reach__router" "^1.3.7" + core-js "^3.0.1" + fast-deep-equal "^3.1.1" + global "^4.3.2" + lodash "^4.17.15" + memoizerific "^1.11.3" + regenerator-runtime "^0.13.7" + store2 "^2.7.1" + telejson "^5.0.2" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/channel-postmessage@6.1.15": version "6.1.15" resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.1.15.tgz#80ea2346d18496f9710dd7f87fd2a9eca46ef36f" @@ -4890,6 +4930,15 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/channels@6.1.21": + version "6.1.21" + resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.1.21.tgz#adbfae5f4767234c5b17d9578be983584dddead4" + integrity sha512-7WoizMjyHqCyvcWncLexSg9FLPIErWAZL4NvluEthwsHSO2sDybn9mh1pzsFHdYMuTP6ml06Zt9ayWMtIveHDg== + dependencies: + core-js "^3.0.1" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/client-api@6.1.15": version "6.1.15" resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.1.15.tgz#8f8ead111459b94621571bdb2276f8a0aace17b1" @@ -4970,6 +5019,14 @@ core-js "^3.0.1" global "^4.3.2" +"@storybook/client-logger@6.1.21": + version "6.1.21" + resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.1.21.tgz#fe7d9e645ddb4eb9dc18fdacea24b4baf11bc6c9" + integrity sha512-QJV+gnVM2fQ4M7lSkRLCXkOw/RU+aEtUefo9TAnXxPHK3UGG+DyvLmha6fHGaz9GAcFxyWtgqCyVOhMe03Q35g== + dependencies: + core-js "^3.0.1" + global "^4.3.2" + "@storybook/components@6.1.15": version "6.1.15" resolved "https://registry.npmjs.org/@storybook/components/-/components-6.1.15.tgz#b4a2af23ee6b9cba4c255191eae3d3463e29bfb7" @@ -5077,6 +5134,13 @@ dependencies: core-js "^3.0.1" +"@storybook/core-events@6.1.21": + version "6.1.21" + resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.1.21.tgz#11f537f78f8c73ba5e627b57b282a279793a3511" + integrity sha512-KWqnh1C7M1pT//WfQb3AD60yTR8jL48AfaeLGto2gO9VK7VVgj/EGsrXZP/GTL90ygyExbbBI5gkr7EBTu/HYw== + dependencies: + core-js "^3.0.1" + "@storybook/core@6.1.15": version "6.1.15" resolved "https://registry.npmjs.org/@storybook/core/-/core-6.1.15.tgz#7ff8c314d3857497bf2e26c69a1fa93ef37301aa" @@ -5277,6 +5341,18 @@ memoizerific "^1.11.3" qs "^6.6.0" +"@storybook/router@6.1.21": + version "6.1.21" + resolved "https://registry.npmjs.org/@storybook/router/-/router-6.1.21.tgz#0a822fa9cc67589a082f7a10fff15c8413f17706" + integrity sha512-m75WvUhoCBWDVekICAdbkidji/w5hCjHo+M8L13UghpwXWEnyr4/QqvkOb/PcSC8aZzxeMqSCpRQ1o6LWULneg== + dependencies: + "@reach/router" "^1.3.3" + "@types/reach__router" "^1.3.7" + core-js "^3.0.1" + global "^4.3.2" + memoizerific "^1.11.3" + qs "^6.6.0" + "@storybook/semver@^7.3.2": version "7.3.2" resolved "https://registry.npmjs.org/@storybook/semver/-/semver-7.3.2.tgz#f3b9c44a1c9a0b933c04e66d0048fcf2fa10dac0" @@ -5374,6 +5450,24 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" +"@storybook/theming@6.1.21": + version "6.1.21" + resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.1.21.tgz#b8e612e5a39b77f7e63a5f9ea322ed62adb0d5b0" + integrity sha512-yq7+/mpdljRdSRJYw/In/9tnDGXIUDe//mhyMftFfrB2mq6zi1yAZpowCerWhiDE2ipGkrfzIYx/Sn7bcaXgqg== + dependencies: + "@emotion/core" "^10.1.1" + "@emotion/is-prop-valid" "^0.8.6" + "@emotion/styled" "^10.0.23" + "@storybook/client-logger" "6.1.21" + core-js "^3.0.1" + deep-object-diff "^1.1.0" + emotion-theming "^10.0.19" + global "^4.3.2" + memoizerific "^1.11.3" + polished "^3.4.4" + resolve-from "^5.0.0" + ts-dedent "^2.0.0" + "@storybook/ui@6.1.15": version "6.1.15" resolved "https://registry.npmjs.org/@storybook/ui/-/ui-6.1.15.tgz#a0f6c49fcf81cf172cd2de4c8dba2be1296891f6" From 5f85dd34797ea2cb79fdac5f42d87fb03c7a2aca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 5 Mar 2021 09:39:26 +0100 Subject: [PATCH 147/147] yarn.lock missing changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- yarn.lock | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/yarn.lock b/yarn.lock index bf55104a3b..0059ae53c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1998,31 +1998,6 @@ "@types/react" "^16.9" classnames "^2.2.6" git-url-parse "^11.4.4" - moment "^2.26.0" - react "^16.13.1" - react-dom "^16.13.1" - react-helmet "6.1.0" - react-router "6.0.0-beta.0" - react-router-dom "6.0.0-beta.0" - react-use "^15.3.3" - swr "^0.3.0" - -"@backstage/plugin-scaffolder@^0.4.1": - version "0.4.2" - resolved "https://registry.npmjs.org/@backstage/plugin-scaffolder/-/plugin-scaffolder-0.4.2.tgz#58159227997f7e248ce52535bc32f19fcd0990dc" - integrity sha512-YuyHM587Rqg6KufxfFqQdI7dsZniBM/11Aj8Q0m5ZszOpCuNmDDkR1VX8MKHTBJ709mnLAqRgArdla7FOrOAXQ== - dependencies: - "@backstage/catalog-client" "^0.3.6" - "@backstage/catalog-model" "^0.7.1" - "@backstage/core" "^0.6.2" - "@backstage/plugin-catalog-react" "^0.0.4" - "@backstage/theme" "^0.2.3" - "@material-ui/core" "^4.11.0" - "@material-ui/icons" "^4.9.1" - "@material-ui/lab" "4.0.0-alpha.45" - "@types/react" "^16.9" - classnames "^2.2.6" - git-url-parse "^11.4.4" react "^16.13.1" react-dom "^16.13.1" react-helmet "6.1.0" @@ -21471,7 +21446,7 @@ qs@6.7.0: resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== -qs@^6.5.1, qs@^6.6.0, qs@^6.7.0, qs@^6.9.1, qs@^6.9.4: +qs@^6.5.1, qs@^6.5.2, qs@^6.6.0, qs@^6.7.0, qs@^6.9.1, qs@^6.9.4: version "6.9.6" resolved "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee" integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==