├── .changeset ├── README.md ├── config.json └── gold-jeans-lay.md ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_template.md │ └── feature_template.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ ├── dependency-review.yml │ ├── release-prepare.yml │ ├── release-publish.yml │ ├── renovate-changesets.yml │ ├── renovate-validation.yml │ └── weekly-check-versions.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .vscode └── settings.json ├── .yarn └── releases │ └── yarn-4.1.1.cjs ├── .yarnrc.yml ├── ADOPTERS.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DCO ├── LICENSE ├── README.md ├── SECURITY.md ├── app-config.production.yaml ├── app-config.yaml ├── backstage.json ├── package.json ├── packages ├── .eslintrc.js ├── app │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── e2e-tests │ │ ├── entityScores.test.ts │ │ └── scoreboard.test.ts │ ├── package.json │ ├── public │ │ ├── android-chrome-192x192.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── manifest.json │ │ ├── robots.txt │ │ └── safari-pinned-tab.svg │ └── src │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── apis.ts │ │ ├── components │ │ ├── Root │ │ │ ├── LogoFull.tsx │ │ │ ├── LogoIcon.tsx │ │ │ ├── Root.tsx │ │ │ └── index.ts │ │ ├── catalog │ │ │ └── EntityPage.tsx │ │ └── home │ │ │ └── HomePage.tsx │ │ ├── index.tsx │ │ └── setupTests.ts ├── backend │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── Dockerfile │ ├── README.md │ ├── package.json │ └── src │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── plugins │ │ ├── app.ts │ │ ├── auth.ts │ │ ├── catalog.ts │ │ ├── healthcheck.ts │ │ ├── proxy.ts │ │ └── scaffolder.ts │ │ └── types.ts └── entities │ ├── publish-github-template.yaml │ ├── s3-upload-template.yaml │ ├── test-entity.yaml │ └── test-template.yaml ├── playwright.config.ts ├── plugins ├── .eslintrc.js └── score-card │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── README.md │ ├── config.d.ts │ ├── dev │ ├── app-config.yaml │ ├── index.tsx │ └── sample-entities.ts │ ├── docs │ └── .assets │ │ ├── score-board.png │ │ ├── score-card-detail.png │ │ ├── score-card-table.png │ │ ├── score-card.png │ │ ├── sharepoint-form-editation.png │ │ └── system-scoring.pdf │ ├── package.json │ ├── sample-data │ ├── @template.json │ ├── README.md │ ├── all.json │ ├── custom-annotation-location │ │ ├── service.json │ │ └── system.json │ └── default │ │ ├── api │ │ └── hello-world.json │ │ └── system │ │ ├── audio-playback.json │ │ └── podcast.json │ ├── src │ ├── api │ │ ├── ScoringDataApi.ts │ │ ├── ScoringDataJsonClient.test.ts │ │ ├── ScoringDataJsonClient.ts │ │ ├── index.ts │ │ └── types.ts │ ├── components │ │ ├── EntityScoreCardTable │ │ │ ├── EntityScoreCardTable.test.tsx │ │ │ ├── EntityScoreCardTable.tsx │ │ │ └── index.ts │ │ ├── ScoreBoardPage │ │ │ ├── ScoreBoardPage.tsx │ │ │ └── index.ts │ │ ├── ScoreCard │ │ │ ├── ScoreCard.test.tsx │ │ │ ├── ScoreCard.tsx │ │ │ ├── columns │ │ │ │ ├── areaColumn.tsx │ │ │ │ ├── detailsColumn.tsx │ │ │ │ ├── scorePercentColumn.tsx │ │ │ │ └── titleColumn.tsx │ │ │ ├── helpers │ │ │ │ ├── getScoreTableEntries.ts │ │ │ │ ├── getWikiUrl.test.ts │ │ │ │ └── getWikiUrl.ts │ │ │ ├── index.ts │ │ │ └── sub-components │ │ │ │ └── getReviewerLink.tsx │ │ └── ScoreCardTable │ │ │ ├── ScoreCardTable.test.tsx │ │ │ ├── ScoreCardTable.tsx │ │ │ └── index.ts │ ├── config │ │ ├── DisplayConfig.test.ts │ │ ├── DisplayConfig.ts │ │ └── types.ts │ ├── helpers │ │ ├── getWarningPanel.tsx │ │ └── scoreToColorConverter.ts │ ├── index.test.ts │ ├── index.ts │ ├── plugin.test.ts │ ├── plugin.ts │ ├── routes.ts │ └── setupTests.ts │ └── tools │ └── azure-devops-pipelines │ ├── README.md │ ├── integration-with-sharepoint.yaml │ ├── scripts │ ├── Sync-Sharepoint.ps1 │ ├── Update-SharepointSelfAssessmentList.ps1 │ ├── functions │ │ ├── Connect-Sharepoint.ps1 │ │ ├── Get-Success.ps1 │ │ └── Read-FromSharepoint.ps1 │ └── push-changes.ps1 │ └── templates │ └── checkout-branch-properly.yaml ├── renovate.json ├── scripts ├── check-if-release.js ├── check-type-dependencies.js ├── copyright-header.txt ├── create-github-release.js ├── create-release-tag.js └── prepare-release.js ├── tsconfig.json └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.2/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "restricted", 7 | "baseBranch": "main", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [] 10 | } 11 | -------------------------------------------------------------------------------- /.changeset/gold-jeans-lay.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@oriflame/backstage-plugin-score-card': minor 3 | --- 4 | 5 | Configure display policies to hide/show owner and kind columns 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var path = require('path'); 18 | 19 | module.exports = { 20 | root: true, 21 | plugins: ['notice'], 22 | rules: { 23 | 'notice/notice': [ 24 | 'error', 25 | { 26 | // eslint-disable-next-line no-restricted-syntax 27 | templateFile: path.resolve(__dirname, './scripts/copyright-header.txt'), 28 | templateVars: { 29 | NAME: 'Oriflame', 30 | }, 31 | varRegexps: { NAME: /(Oriflame)/ }, 32 | onNonMatchingHeader: 'replace', 33 | }, 34 | ], 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Bug Report' 3 | about: 'Create Bug Report' 4 | labels: kind/bug 5 | --- 6 | 7 | 8 | 9 | ## Expected Behavior 10 | 11 | 12 | 13 | ## Current Behavior 14 | 15 | 16 | 17 | ## Steps to Reproduce 18 | 19 | 20 | 21 | 22 | ## Possible Solution 23 | 24 | 25 | 26 | 27 | ## Context 28 | 29 | 30 | 31 | 32 | 33 | ## Your Environment 34 | 35 | Output of `yarn backstage-cli info`: 36 | 37 | ```text 38 | 39 | TODO: paste here output from above command 40 | 41 | ``` 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Feature Request' 3 | about: 'Suggest new features and changes' 4 | labels: kind/enhancement 5 | --- 6 | 7 | 8 | 9 | ## Feature Suggestion 10 | 11 | 12 | 13 | ## Possible Implementation 14 | 15 | 16 | 17 | ## Context 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | #### :heavy_check_mark: Checklist 4 | 5 | - [ ] Added tests for new functionality and regression tests for bug fixes 6 | - [ ] Added changeset (run `yarn changeset` in the root) 7 | - [ ] Screenshots of before and after attached (for UI changes) 8 | - [ ] Added or updated documentation (if applicable) 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: 'CI workflow' 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request_target: 6 | branches: 7 | - main 8 | push: 9 | branches: 10 | - main 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | # TODO: template for build to DRY for CI and for publish workflows 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | node-version: [20.x] 22 | steps: 23 | - name: Checkout non PR 24 | uses: actions/checkout@v4.1.4 25 | # Do not trigger a checkout when opening PRs from a fork (helps avoid 26 | # "pwn request". See https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request_target ) 27 | if: github.event_name != 'pull_request_target' 28 | with: 29 | fetch-depth: '0' 30 | 31 | - name: Checkout PR 32 | uses: actions/checkout@v4.1.4 33 | if: github.event_name == 'pull_request_target' 34 | with: 35 | ref: "refs/pull/${{ github.event.number }}/merge" 36 | fetch-depth: '0' 37 | 38 | - name: Use Node.js ${{ matrix.node-version }} 39 | uses: actions/setup-node@v4.0.2 40 | with: 41 | node-version: ${{ matrix.node-version }} 42 | - run: yarn install --frozen-lockfile 43 | 44 | - name: validate config 45 | run: yarn backstage-cli config:check --lax 46 | 47 | - name: lint 48 | run: yarn backstage-cli repo lint 49 | 50 | - name: type checking and declarations 51 | run: yarn tsc:full 52 | 53 | - name: build 54 | run: yarn backstage-cli repo build --all 55 | env: 56 | CI: true 57 | 58 | - name: verify type dependencies 59 | run: yarn lint:type-deps 60 | 61 | - name: test 62 | run: | 63 | yarn backstage-cli repo test --maxWorkers=3 --workerIdleMemoryLimit=1300M 64 | 65 | # Playwright e2e tests 66 | - name: Install Playwright Browsers 67 | run: yarn playwright install --with-deps 68 | 69 | - name: Run Playwright tests 70 | run: yarn test:e2e 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | 74 | - name: Upload e2e tests result 75 | uses: actions/upload-artifact@v4 76 | if: always() 77 | with: 78 | name: e2e-test-report 79 | path: e2e-test-report/ 80 | retention-days: 30 81 | include-hidden-files: true 82 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: 9 | pull_request_target: 10 | branches: 11 | - main 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | dependency-review: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: 'Checkout Repository' 21 | uses: actions/checkout@v4.1.4 22 | - name: 'Dependency Review' 23 | uses: actions/dependency-review-action@v3 24 | -------------------------------------------------------------------------------- /.github/workflows/release-prepare.yml: -------------------------------------------------------------------------------- 1 | name: Prepare release PR workflow 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | create-release-pr: 10 | name: Create Changeset PR 11 | strategy: 12 | matrix: 13 | node-version: [20.x] 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4.1.4 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v4.0.2 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - name: Install Dependencies 24 | run: yarn --frozen-lockfile 25 | 26 | - name: Create Release Pull Request 27 | uses: changesets/action@v1 28 | with: 29 | # Calls out to `changeset version`, but also runs prettier 30 | version: yarn release 31 | title: 'Release new version(s)' 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/release-publish.yml: -------------------------------------------------------------------------------- 1 | name: 'Release and publish Workflow' 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | create_release: 7 | description: 'Create release' 8 | required: true 9 | type: boolean 10 | default: true 11 | publish_packages: 12 | description: 'Publish npm packages' 13 | required: true 14 | type: boolean 15 | default: true 16 | push: 17 | branches: [main] 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | 23 | outputs: 24 | needs_release: ${{ steps.release_check.outputs.needs_release }} 25 | 26 | strategy: 27 | matrix: 28 | node-version: [20.x] 29 | 30 | env: 31 | CI: true 32 | NODE_OPTIONS: --max-old-space-size=4096 33 | 34 | steps: 35 | - name: Check of github.event.before 36 | if: ${{ !github.event.inputs.create_release && !github.event.inputs.publish_packages }} 37 | run: if [ '${{ github.event.before }}' = '0000000000000000000000000000000000000000' ]; then echo "::warning title=Missing github.event.before::You are running this CD workflow on a newly created branch. Release won't be created..."; fi 38 | 39 | - name: Checkout repository 40 | uses: actions/checkout@v4.1.4 41 | 42 | - name: use node.js ${{ matrix.node-version }} 43 | uses: actions/setup-node@v4.0.2 44 | with: 45 | node-version: ${{ matrix.node-version }} 46 | registry-url: https://registry.npmjs.org/ # Needed for auth 47 | 48 | - name: yarn install --frozen-lockfile 49 | uses: backstage/actions/yarn-install@v0.6.11 50 | with: 51 | cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} 52 | 53 | - name: Fetch previous commit for release check 54 | if: ${{ github.event.before != '0000000000000000000000000000000000000000' }} 55 | run: git fetch origin '${{ github.event.before }}' 56 | 57 | - name: Check if release 58 | id: release_check 59 | if: ${{ github.event.before != '0000000000000000000000000000000000000000' }} 60 | run: node scripts/check-if-release.js 61 | env: 62 | COMMIT_SHA_BEFORE: '${{ github.event.before }}' 63 | 64 | - name: validate config 65 | run: yarn backstage-cli config:check --lax 66 | 67 | - name: lint 68 | run: yarn backstage-cli repo lint 69 | 70 | - name: type checking and declarations 71 | run: yarn tsc:full 72 | 73 | - name: build 74 | run: yarn backstage-cli repo build --all 75 | 76 | - name: verify type dependencies 77 | run: yarn lint:type-deps 78 | 79 | 80 | # A separate release build that is only run for commits that are the result of merging the "Version Packages" PR 81 | # We can't re-use the output from the above step, but we'll have a guaranteed node_modules cache and 82 | # only run the build steps that are necessary for publishing 83 | release: 84 | needs: build 85 | 86 | if: needs.build.outputs.needs_release == 'true' || github.event.inputs.create_release || github.event.inputs.publish_packages 87 | 88 | runs-on: ubuntu-latest 89 | 90 | strategy: 91 | matrix: 92 | node-version: [20.x] 93 | 94 | env: 95 | CI: 'true' 96 | NODE_OPTIONS: --max-old-space-size=4096 97 | 98 | steps: 99 | - name: Checkout repository 100 | uses: actions/checkout@v4.1.4 101 | 102 | - name: use node.js ${{ matrix.node-version }} 103 | uses: actions/setup-node@v4.0.2 104 | with: 105 | node-version: ${{ matrix.node-version }} 106 | registry-url: https://registry.npmjs.org/ # Needed for auth 107 | 108 | - name: yarn install --frozen-lockfile 109 | uses: backstage/actions/yarn-install@v0.6.11 110 | with: 111 | cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} 112 | 113 | - name: build type declarations 114 | run: yarn tsc:full 115 | 116 | - name: build packages 117 | run: yarn backstage-cli repo build 118 | 119 | # Publishes current version of packages that are not already present in the registry 120 | - name: publish 121 | if: needs.build.outputs.needs_release == 'true' || github.event.inputs.publish_packages 122 | run: | 123 | yarn config set -H 'npmAuthToken' "${{secrets.NPM_TOKEN}}" 124 | if [ -f ".changeset/pre.json" ]; then 125 | yarn workspaces foreach --all --verbose --no-private npm publish --access public --tolerate-republish --tag next 126 | else 127 | yarn workspaces foreach --all --verbose --no-private npm publish --access public --tolerate-republish 128 | fi 129 | env: 130 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 131 | 132 | # Grabs the version in the root package.json and creates a tag on GitHub 133 | - name: Create a release tag 134 | id: create_tag 135 | if: needs.build.outputs.needs_release == 'true' || github.event.inputs.create_release 136 | run: node scripts/create-release-tag.js 137 | env: 138 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 139 | 140 | # Convert the newly created tag into a release with changelog information 141 | - name: Create release on GitHub 142 | if: needs.build.outputs.needs_release == 'true' || github.event.inputs.create_release 143 | run: node scripts/create-github-release.js ${{ steps.create_tag.outputs.tag_name }} 1 144 | env: 145 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 146 | -------------------------------------------------------------------------------- /.github/workflows/renovate-changesets.yml: -------------------------------------------------------------------------------- 1 | name: 'Renovate: Generate changeset' 2 | on: 3 | pull_request_target: 4 | paths: 5 | - '.github/workflows/renovate-changesets.yml' 6 | - '**/yarn.lock' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | generate-changeset: 13 | runs-on: ubuntu-latest 14 | if: github.actor == 'renovate[bot]' 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4.1.4 18 | with: 19 | fetch-depth: 2 20 | ref: ${{ github.head_ref }} 21 | token: ${{ secrets.GITHUB_TOKEN }} 22 | - name: Configure Git 23 | run: | 24 | git config --global user.email noreply@oriflame.com 25 | git config --global user.name 'Github changeset workflow' 26 | - name: Generate changeset 27 | uses: actions/github-script@v6 28 | with: 29 | script: | 30 | const { promises: fs } = require("fs"); 31 | // Parses package.json files and returns the package names 32 | async function getPackagesNames(files) { 33 | const names = []; 34 | for (const file of files) { 35 | const data = JSON.parse(await fs.readFile(file, "utf8")); 36 | if (!data.private) { 37 | names.push(data.name); 38 | } 39 | } 40 | return names; 41 | } 42 | 43 | async function createChangeset(fileName, packageBumps, packages) { 44 | let message = ""; 45 | for (const [pkg, bump] of packageBumps) { 46 | message = message + `Updated dependency \`${pkg}\` to \`${bump}\`.\n`; 47 | } 48 | 49 | const pkgs = packages.map((pkg) => `'${pkg}': patch`).join("\n"); 50 | const body = `---\n${pkgs}\n---\n\n${message.trim()}\n`; 51 | await fs.writeFile(fileName, body); 52 | } 53 | 54 | async function getBumps(files) { 55 | const bumps = new Map(); 56 | for (const file of files) { 57 | const { stdout: changes } = await exec.getExecOutput("git", [ 58 | "show", 59 | file, 60 | ]); 61 | for (const change of changes.split("\n")) { 62 | if (!change.startsWith("+ ")) { 63 | continue; 64 | } 65 | const match = change.match(/"(.*?)"/g); 66 | bumps.set(match[0].replace(/"/g, ""), match[1].replace(/"/g, "")); 67 | } 68 | } 69 | return bumps; 70 | } 71 | 72 | const branch = await exec.getExecOutput("git branch --show-current"); 73 | if (!branch.stdout.startsWith("renovate/")) { 74 | console.log("Not a renovate branch, skipping"); 75 | return; 76 | } 77 | const diffOutput = await exec.getExecOutput("git diff --name-only HEAD~1"); 78 | const diffFiles = diffOutput.stdout.split("\n"); 79 | if (diffFiles.find((f) => f.startsWith(".changeset"))) { 80 | console.log("Changeset already exists, skipping"); 81 | return; 82 | } 83 | const files = diffFiles 84 | .filter((file) => file !== "package.json") // skip root package.json 85 | .filter((file) => file.includes("package.json")); 86 | const packageNames = await getPackagesNames(files); 87 | if (!packageNames.length) { 88 | console.log("No package.json changes to published packages, skipping"); 89 | return; 90 | } 91 | const { stdout: shortHash } = await exec.getExecOutput( 92 | "git rev-parse --short HEAD" 93 | ); 94 | const fileName = `.changeset/renovate-${shortHash.trim()}.md`; 95 | 96 | const packageBumps = await getBumps(files); 97 | await createChangeset(fileName, packageBumps, packageNames); 98 | await exec.exec("git", ["add", fileName]); 99 | await exec.exec("git commit -C HEAD --amend --no-edit"); 100 | await exec.exec("git push --force"); 101 | -------------------------------------------------------------------------------- /.github/workflows/renovate-validation.yml: -------------------------------------------------------------------------------- 1 | name: 'Renovate: Validate configuration' 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | paths: 8 | - 'renovate.json' 9 | push: 10 | branches: 11 | - main 12 | paths: 13 | - 'renovate.json' 14 | 15 | jobs: 16 | validate: 17 | name: Validate 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4.1.4 22 | - name: Validate 23 | uses: rinchsan/renovate-config-validator@v0.0.12 24 | with: 25 | pattern: 'renovate.json' 26 | -------------------------------------------------------------------------------- /.github/workflows/weekly-check-versions.yml: -------------------------------------------------------------------------------- 1 | name: Check bump versions 2 | on: 3 | schedule: 4 | - cron: '0 6 * * 5' # At 06:00 on Friday 5 | workflow_dispatch: 6 | 7 | jobs: 8 | create-pull-request: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4.1.4 12 | with: 13 | fetch-depth: '0' 14 | 15 | - name: Install dependencies and bump versions 16 | run: | 17 | yarn install 18 | yarn backstage-cli versions:bump 19 | - name: Get current time 20 | uses: 1466587594/get-current-time@v2 21 | id: current-time 22 | with: 23 | format: YYYYMMDD-HH-MM 24 | - name: Create Pull Request 25 | id: cpr 26 | uses: peter-evans/create-pull-request@v3 27 | with: 28 | token: ${{ secrets.GITHUB_TOKEN }} 29 | commit-message: Update backstage core dependencies 30 | committer: GitHub 31 | author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> 32 | signoff: false 33 | branch: update-backstage-core-deps-${{ steps.current-time.outputs.formattedTime }} 34 | delete-branch: true 35 | title: 'Update Backstage core dependencies - plugins' 36 | base: main 37 | body: | 38 | Update backstage dependencies 39 | - Updated using backstage-cli versions:bump 40 | - Auto-generated by [create-pull-request][1] 41 | 42 | [1]: https://github.com/peter-evans/create-pull-request 43 | labels: | 44 | dependency 45 | update 46 | automated pr 47 | draft: false 48 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | engine-strict=true 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .yarn 2 | dist 3 | microsite 4 | coverage 5 | *.hbs 6 | templates 7 | api-report.md 8 | cli-report.md 9 | plugins/scaffolder-backend/sample-templates 10 | .vscode 11 | dist-types 12 | 13 | # reduce the barrier for adopters to add themselves 14 | ADOPTERS.md 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "jest.jestCommandLine": "node_modules/.bin/jest --config node_modules/@backstage/cli/config/jest.js", 3 | 4 | } -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableGlobalCache: true 2 | 3 | httpTimeout: 300000 4 | 5 | nodeLinker: node-modules 6 | 7 | npmRegistryServer: "https://registry.npmjs.org/" 8 | 9 | yarnPath: .yarn/releases/yarn-4.1.1.cjs 10 | -------------------------------------------------------------------------------- /ADOPTERS.md: -------------------------------------------------------------------------------- 1 | | Organization | Contact | Description of Use | 2 | | --------------------------- | ------------------------------------------ | ----------------------------- | 3 | | [Oriflame](https://www.linkedin.com/company/oriflame-software) | [@jvilimek](https://github.com/jvilimek) | Development | 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We subscribe to the same [Spotify FOSS Code of Conduct](https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md) that Backstage uses. 4 | 5 | ## Reporting issues 6 | 7 | If you experience or witness unacceptable behavior—or have any other concerns—please report it by contacting us, see [Oriflame GitHub Home Page](https://github.com/Oriflame)). All reports will be handled with discretion. In your report please include: 8 | 9 | - Your contact information. 10 | - Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional witnesses, please include them as well. Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public IRC logger), please include a link. 11 | - Any additional information that may be helpful. 12 | 13 | After filing a report, a representative will contact you personally, review the incident, follow up with any additional questions, and make a decision as to how to respond. If the person who is harassing you is part of the response team, they will recuse themselves from handling your incident. If the complaint originates from a member of the response team, it will be handled by a different member of the response team. We will respect confidentiality requests for the purpose of protecting victims of abuse. 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Oriflame plugins for Backstage 2 | 3 | We want to create strong community of contributors -- all working together to create the kind of delightful experience that Backstage deserves. 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. ❤️ 6 | 7 | In general, we aim to stick as closely as possible to the [contribution guidelines which apply to the Backstage project](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md). If something is not covered in this document, please assume that the appropriate Backstage guideline will apply. 8 | 9 | ## Types of Contributions 10 | 11 | This repository will gather the plugins we have worked on, so contribution is more than welcome both in this repository in general, and in a scope of a particular plugin. 12 | 13 | ### Report bugs 14 | 15 | No one likes bugs. Report bugs as an issue [here](https://github.com/Oriflame/backstage-plugins/issues/new?assignees=&labels=bug&template=bug_template.md). 16 | 17 | ### Fix bugs or build new features 18 | 19 | Look through the GitHub issues for bugs or problems that other users are having. If you're having a problem yourself, feel free to contribute a fix for us to review. 20 | 21 | ### Submit feedback 22 | 23 | The best way to send feedback is to file [an issue](https://github.com/Oriflame/backstage-plugins/issues/new). 24 | 25 | If you are proposing a feature: 26 | 27 | - Explain in detail how it would work. 28 | - Explain the wider context about what you are trying to achieve. 29 | - Keep the scope as narrow as possible, to make it easier to implement. 30 | - Remember that this is a volunteer-driven project, and that contributions are welcome :) 31 | 32 | ### Write E2E tests 33 | 34 | As the number of plugins included in this repository increases, so does importance of good E2E tests which will make sure everything runs as it is expected. In order to contribute to this, very important aspect, of this repository, we urge you to follow guidelines below: 35 | 36 | E2E tests are integrated under `/packages/app/e2e-tests` folder where you will find specific E2E test for every plugin. This means you should follow that pattern and add tests in appropriate plugin test files. For testing purposes you can use `test-entity.yaml` file which can be found under `/packages/entities`, which we have created especially for this purpose. 37 | 38 | As an engine for the testing we are using [Playwright](https://playwright.dev/). 39 | 40 | To execute end to end test locally you would need to install [dependencies](https://playwright.dev/docs/browsers#install-system-dependencies). TL/DR: run `npx playwright install-deps`. 41 | 42 | ### Add your company to ADOPTERS 43 | 44 | Have you started using any/some/all of our plugins? Adding your company to [ADOPTERS](https://github.com/Oriflame/backstage-plugins/blob/main/ADOPTERS.md) really helps the project. 45 | 46 | ## Get Started 47 | 48 | So...feel ready to jump in? Let's do this. 💯 👏 49 | 50 | Start by reading repository README to get set up for local development. If you need help, just jump into [our GitHub discussions](https://github.com/Oriflame/backstage-plugins/discussions). 51 | 52 | ## Commits signing 53 | 54 | We do require that all commits are signed. How to sign commits: [signing-commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). 55 | 56 | Don't forget to specify correct git user for signature, e.g. run `git config user.email jvilimek@users.noreply.github.com`. 57 | 58 | See also [telling-git-about-your-signing-key](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key). 59 | 60 | ## Coding Guidelines 61 | 62 | We use the backstage-cli to build, serve, lint, test and package all the plugins. 63 | 64 | As such, the [same coding guidelines as in Backstage repository mostly apply](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md#coding-guidelines). 65 | 66 | ## Creating Changesets 67 | 68 | We use [changesets](https://github.com/atlassian/changesets) in order to prepare releases. To make the process of generating releases easy, please include changesets with your pull request. This will result in a every package affected by a change getting a proper version number and an entry in its `CHANGELOG.md. 69 | 70 | ### When to use a changeset? 71 | 72 | Any time a patch, minor, or major change aligning to [Semantic Versioning](https://semver.org) is made to any published package in `plugins/`, a changeset should be used. 73 | In general, changesets are not needed for the documentation, build utilities or similar. 74 | 75 | ### How to create a changeset 76 | 77 | 1. Run `yarn changeset` 78 | 2. Select which packages you want to include a changeset for 79 | 3. Select impact of change that you're introducing, using `minor` for breaking changes and `patch` otherwise. 80 | 4. Explain your changes in the generated changeset. See [examples of well written changesets](https://backstage.io/docs/getting-started/contributors#writing-changesets). 81 | 5. Add generated changeset to Git 82 | 6. Push the commit with your changeset to the branch associated with your PR 83 | 84 | For more information, checkout [adding a changeset](https://github.com/atlassian/changesets/blob/master/docs/adding-a-changeset.md) documentation in the changesets repository. 85 | 86 | ## Releasing Plugins and Packages 87 | 88 | Please include changeset files your pull requests if you would like them to be released. To create a changeset file run `yarn changeset` and commit the resulting file to the pull request. 89 | 90 | After merging a changeset file to main, a subsequent pull request is created automatically that makes the actual version bumps of the plugins/packages based on the changeset files. When this pull request is merged, the plugins and packages are automatically published to npm. 91 | 92 | ## Code of Conduct 93 | 94 | We subscribe to the [Spotify FOSS code of conduct](https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md) which is used by the Backstage project. 95 | 96 | If you experience or witness unacceptable behavior—or have any other concerns—please report it by contacting us, see [Oriflame GitHub Home Page](https://github.com/Oriflame). 97 | 98 | ## Security Issues? 99 | 100 | See [SECURITY.md](https://github.com/Oriflame/backstage-plugins/blob/main/SECURITY.md) 101 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 1 Letterman Drive 6 | Suite D4700 7 | San Francisco, CA, 94129 8 | 9 | Everyone is permitted to copy and distribute verbatim copies of this 10 | license document, but changing it is not allowed. 11 | 12 | 13 | Developer's Certificate of Origin 1.1 14 | 15 | By making a contribution to this project, I certify that: 16 | 17 | (a) The contribution was created in whole or in part by me and I 18 | have the right to submit it under the open source license 19 | indicated in the file; or 20 | 21 | (b) The contribution is based upon previous work that, to the best 22 | of my knowledge, is covered under an appropriate open source 23 | license and I have the right under that license to submit that 24 | work with modifications, whether created in whole or in part 25 | by me, under the same open source license (unless I am 26 | permitted to submit under a different license), as indicated 27 | in the file; or 28 | 29 | (c) The contribution was provided directly to me by some other 30 | person who certified (a), (b) or (c) and I have not modified 31 | it. 32 | 33 | (d) I understand and agree that this project and the contribution 34 | are public and that a record of the contribution (including all 35 | personal information I submit with it, including my sign-off) is 36 | maintained indefinitely and may be redistributed consistent with 37 | this project or the open source license(s) involved. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # backstage-plugins 2 | 3 | Oriflame Backstage plugins. 4 | 5 | ## Getting started 6 | 7 | You may find our plugins in the `./plugins` folder. You may start each plugin in isolated mode (navigate to the plugin folder and run `yarn dev` or `yarn start:dev`, see respective README). You may start also the simple backstage host with the plugins integrated via `yarn dev` (in root folder). You may run `yarn test` to run jest tests. For more information see [CONTRIBUTING.md](./CONTRIBUTING.md). 8 | 9 | List of prerequisites are [same as for the backstage](https://backstage.io/docs/getting-started/#prerequisites). Please use Node.js version `20.x`. 10 | 11 | ## List of plugins 12 | 13 | Name | Version | Description 14 | ---------|----------|---------- 15 | [score-card](https://github.com/Oriflame/backstage-plugins/blob/main/plugins/score-card/README.md) | [![npm version](https://badge.fury.io/js/@oriflame%2Fbackstage-plugin-score-card.svg)](https://badge.fury.io/js/@oriflame%2Fbackstage-plugin-score-card) | Main idea behind it comes from a need to somehow visualize maturity of our services and to establish a process how to improve it (discuss with the teams what to focus on next). 16 | 17 | ## Workflows 18 | 19 | We use GitHub actions to check build, unit & end to end test and other validations during pull requests. We use them also to prepare releases and publish npm packages. 20 | 21 | In overview: 22 | 23 | - create branch, commit changes, run `yarn changeset`, commit and create PR -> [CI workflow](#ci-workflow) will run 24 | - once merged to `main` (on push) [Prepare release PR workflow](#prepare-release-pr-workflow) -> `Release new version(s)` pull request is created automatically. It shall increase versions of packages and update changelogs in respective plugins and cleanup the `.changeset` folder. 25 | - once this PR is merged to `main` [Release and publish Workflow](#release-and-publish-workflow) will create a new release on GitHub and also publishes changed plugins. 26 | 27 | ### CI workflow 28 | 29 | [![CI pipeline](https://github.com/Oriflame/backstage-plugins/actions/workflows/ci.yml/badge.svg)](https://github.com/Oriflame/backstage-plugins/actions/workflows/ci.yml) 30 | 31 | Source: `.github/workflows/ci.yml` 32 | 33 | Shall be executed during `pull requests` to validate changes and also during push to `main` branch to keep validating the main trunk. 34 | 35 | ## Prepare release PR workflow 36 | 37 | [![Prepare release PR workflow](https://github.com/Oriflame/backstage-plugins/actions/workflows/release-prepare.yml/badge.svg)](https://github.com/Oriflame/backstage-plugins/actions/workflows/release-prepare.yml) 38 | 39 | Source: `.github/workflows/release-prepare.yml` 40 | 41 | Shall be executed on push to `main`. It runs `yarn release` = increase versions of packages and update changelogs in respective plugins and cleanup the `.changeset` folder. It comit the changes in a new branch and prepare a new PR `Release new version(s)`. 42 | 43 | ### Release and publish Workflow 44 | 45 | [![Release and publish Workflow](https://github.com/Oriflame/backstage-plugins/actions/workflows/release-publish.yml/badge.svg)](https://github.com/Oriflame/backstage-plugins/actions/workflows/release-publish.yml) 46 | 47 | Source: `.github/workflows/ci.yml` 48 | 49 | Shall be executed on push to `main`. In case the package versions are changed (which are by the previous PR) it creates a new release on GitHub and also publishes changed plugins to npm repository. 50 | 51 | ### Renovate: Validate configuration 52 | 53 | [![Renovate: Validate configuration](https://github.com/Oriflame/backstage-plugins/actions/workflows/renovate-validation.yml/badge.svg)](https://github.com/Oriflame/backstage-plugins/actions/workflows/renovate-validation.yml) 54 | 55 | Source: `.github/workflows/renovate-changesets.yml` 56 | 57 | Shall be executed during `pull requests` when `renovate.json` changes to validate the changes. 58 | 59 | ### Renovate: Generate changeset 60 | 61 | [![Renovate: Generate changeset](https://github.com/Oriflame/backstage-plugins/actions/workflows/renovate-changesets.yml/badge.svg)](https://github.com/Oriflame/backstage-plugins/actions/workflows/renovate-changesets.yml) 62 | 63 | Source: `.github/workflows/renovate-changesets.yml` 64 | 65 | Shall be executed during `pull requests`. In case the package versions are changed (and the PR was created by renovate bot) it pushes automatically generated `.changesets` entries. 66 | 67 | ## Thank you note 68 | 69 | When creating this repository (pipelines, e2e tests, monorepo setup...) we were inspired a lot by a following repository [roadie-backstage-plugins](https://github.com/RoadieHQ/roadie-backstage-plugins). 70 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 0.x | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Please report security issues via contacts on [Oriflame GitHub Home Page](https://github.com/Oriflame). 12 | -------------------------------------------------------------------------------- /app-config.production.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | # Should be the same as backend.baseUrl when using the `app-backend` plugin 3 | baseUrl: http://localhost:7007 4 | 5 | backend: 6 | baseUrl: http://localhost:7007 7 | listen: 8 | port: 7007 9 | -------------------------------------------------------------------------------- /app-config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | title: Scaffolded Backstage App 3 | baseUrl: http://localhost:3000 4 | 5 | organization: 6 | name: My Company 7 | 8 | backend: 9 | baseUrl: http://localhost:7007 10 | 11 | listen: 12 | port: 7007 13 | csp: 14 | connect-src: ["'self'", 'http:', 'https:'] 15 | cors: 16 | origin: http://localhost:3000 17 | methods: [GET, POST, PUT, DELETE] 18 | credentials: true 19 | database: 20 | client: better-sqlite3 21 | connection: ':memory:' 22 | cache: 23 | store: memory 24 | auth: 25 | keys: 26 | - secret: foo 27 | # workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir 28 | 29 | integrations: 30 | github: 31 | - host: github.com 32 | token: ${GITHUB_TOKEN} 33 | ### Example for how to add your GitHub Enterprise instance using the API: 34 | # - host: ghe.example.net 35 | # apiBaseUrl: https://ghe.example.net/api/v3 36 | # token: ${GHE_TOKEN} 37 | 38 | proxy: 39 | 40 | auth: 41 | # see https://backstage.io/docs/auth/ to learn about auth providers 42 | providers: {} 43 | scaffolder: 44 | github: 45 | token: ${GITHUB_TOKEN} 46 | visibility: public # or 'internal' or 'private' 47 | 48 | scorecards: 49 | jsonDataUrl: http://localhost:8090/plugins/score-card/sample-data/ #this is being served via http-server 50 | wikiLinkTemplate: https://link-to-wiki/{id} 51 | 52 | catalog: 53 | rules: 54 | - allow: [Component, System, API, Group, User, Resource, Location, Template] 55 | locations: 56 | # File used for testing purposes 57 | - type: file 58 | target: ../entities/test-entity.yaml 59 | 60 | - type: file 61 | target: ../entities/test-template.yaml 62 | rules: 63 | - allow: [Template] 64 | 65 | # Backstage example components 66 | - type: url 67 | target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-components.yaml 68 | 69 | # Backstage example systems 70 | - type: url 71 | target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-systems.yaml 72 | 73 | # Backstage example APIs 74 | - type: url 75 | target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-apis.yaml 76 | 77 | # Backstage example resources 78 | - type: url 79 | target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-resources.yaml 80 | 81 | # Backstage example organization groups 82 | - type: url 83 | target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme/org.yaml 84 | 85 | - type: url 86 | target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/remote-templates.yaml 87 | rules: 88 | - allow: [Template] 89 | 90 | # Backstage example templates 91 | # - type: url 92 | # target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml 93 | # rules: 94 | # - allow: [Template] 95 | # - type: url 96 | # target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml 97 | # rules: 98 | # - allow: [Template] 99 | # - type: url 100 | # target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml 101 | # rules: 102 | # - allow: [Template] 103 | # - type: url 104 | # target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml 105 | # rules: 106 | # - allow: [Template] 107 | # - type: url 108 | # target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml 109 | # rules: 110 | # - allow: [Template] 111 | -------------------------------------------------------------------------------- /backstage.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.22.1" 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ori-backstage-plugins", 3 | "description": "Oriflame plugins for Backstage.", 4 | "maintainers": [ 5 | "jvilimek@users.noreply.github.com", 6 | "OSWDVLPPlatform@oriflame.com", 7 | "GlobalITCOEdevelopmentTooling@Oriflame.com" 8 | ], 9 | "version": "0.2.3", 10 | "private": true, 11 | "engines": { 12 | "node": "18 || 20" 13 | }, 14 | "scripts": { 15 | "dev": "concurrently 'yarn start' 'yarn start-backend' 'http-server -p 8090 --cors 2>&1'", 16 | "start": "yarn workspace app start", 17 | "start-backend": "yarn workspace backend start", 18 | "build": "backstage-cli repo build --all", 19 | "tsc": "tsc", 20 | "tsc:full": "backstage-cli repo clean && tsc --skipLibCheck true --incremental false", 21 | "clean": "backstage-cli repo clean", 22 | "test": "backstage-cli repo test", 23 | "test:all": "backstage-cli repo test --coverage", 24 | "test:e2e": "tsc && playwright test", 25 | "fix": "backstage-cli repo fix", 26 | "lint": "backstage-cli repo lint --since origin/main", 27 | "lint:all": "backstage-cli repo lint", 28 | "lint:type-deps": "backstage-repo-tools type-deps", 29 | "new": "backstage-cli new --scope ori --baseVersion 0.0.0 --no-private", 30 | "create-plugin": "echo \"use 'yarn new' instead\"", 31 | "release": "node scripts/prepare-release.js && changeset version && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' '.changeset/*.json' && yarn install --no-immutable", 32 | "prettier:check": "prettier --check .", 33 | "prettier:fix": "prettier --write .", 34 | "lock:check": "yarn-lock-check" 35 | }, 36 | "workspaces": { 37 | "packages": [ 38 | "packages/*", 39 | "plugins/*" 40 | ] 41 | }, 42 | "resolutions": { 43 | "@types/react": "^18.2.48", 44 | "@types/react-dom": "^18.2.18", 45 | "dockerode": "3.3.5", 46 | "@types/dockerode": "3.3.28", 47 | "jest-environment-jsdom": "30.0.0-alpha.2" 48 | }, 49 | "dependencies": { 50 | "jest-environment-jsdom": "30.0.0-alpha.2" 51 | }, 52 | "devDependencies": { 53 | "@backstage/cli": "^0.25.1", 54 | "@backstage/e2e-test-utils": "^0.1.0", 55 | "@backstage/repo-tools": "^0.5.2", 56 | "@changesets/cli": "2.27.1", 57 | "@playwright/test": "^1.43.0", 58 | "@spotify/prettier-config": "14.1.6", 59 | "@types/react": "^18.2.48", 60 | "@types/react-dom": "^18.2.18", 61 | "@types/webpack": "5.28.0", 62 | "concurrently": "^8.0.0", 63 | "eslint-plugin-notice": "0.9.10", 64 | "http-server": "14.1.1", 65 | "prettier": "2.8.8", 66 | "react": "^18.0.0", 67 | "typescript": "~5.4.0" 68 | }, 69 | "prettier": "@spotify/prettier-config", 70 | "lint-staged": { 71 | "*.{js,jsx,ts,tsx,mjs,cjs}": [ 72 | "eslint --fix", 73 | "prettier --write" 74 | ], 75 | "*.{json,md}": [ 76 | "prettier --write" 77 | ], 78 | "*.md": [ 79 | "node ./scripts/check-docs-quality" 80 | ] 81 | }, 82 | "packageManager": "yarn@4.1.1" 83 | } 84 | -------------------------------------------------------------------------------- /packages/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame (Based on https://github.com/RoadieHQ/roadie-backstage-plugins source copyrighted by Larder Software Limited) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var path = require('path'); 18 | 19 | module.exports = { 20 | root: true, 21 | plugins: ['notice'], 22 | rules: { 23 | 'notice/notice': [ 24 | 'error', 25 | { 26 | // eslint-disable-next-line no-restricted-syntax 27 | templateFile: path.resolve(__dirname, '../scripts/copyright-header.txt'), 28 | templateVars: { 29 | NAME: 'Oriflame (Based on https://github.com/RoadieHQ/roadie-backstage-plugins source copyrighted by Larder Software Limited)', 30 | }, 31 | varRegexps: { NAME: /(Oriflame \([^\)]+\))/ }, 32 | onNonMatchingHeader: 'replace', 33 | }, 34 | ], 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /packages/app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@backstage/cli/config/eslint')], 3 | overrides: [ 4 | { 5 | files: ['**/*.ts?(x)'], 6 | rules: { 7 | 'react/prop-types': 1, 8 | }, 9 | }, 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /packages/app/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # app 2 | 3 | ## 0.5.1 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [35c2578] 8 | - Updated dependencies [500e616] 9 | - @oriflame/backstage-plugin-score-card@0.9.0 10 | 11 | ## 0.5.0 12 | 13 | ### Minor Changes 14 | 15 | - 32cc2fb: Upgraded to Backstage core 1.22.1, support for React v18, Node 20, Cypress e2e test migrated to [Playwright](https://playwright.dev/) and more. 16 | 17 | ### Patch Changes 18 | 19 | - Updated dependencies [32cc2fb] 20 | - @oriflame/backstage-plugin-score-card@0.8.0 21 | 22 | ## 0.4.7 23 | 24 | ### Patch Changes 25 | 26 | - Updated dependencies [bd30933] 27 | - @oriflame/backstage-plugin-score-card@0.7.0 28 | 29 | ## 0.4.6 30 | 31 | ### Patch Changes 32 | 33 | - e8c6214: Bumped backstage core libs from 1.8.3 to 1.10.1 34 | - Updated dependencies [e8c6214] 35 | - @oriflame/backstage-plugin-score-card@0.6.3 36 | 37 | ## 0.4.5 38 | 39 | ### Patch Changes 40 | 41 | - 398f28d: bump version for backstage core components to 1.8.3 42 | - Updated dependencies [ec3991d] 43 | - Updated dependencies [398f28d] 44 | - @oriflame/backstage-plugin-score-card@0.6.0 45 | 46 | ## 0.4.4 47 | 48 | ### Patch Changes 49 | 50 | - ac868b1: bumped to 1.7.0 backstage core version 51 | - Updated dependencies [ac868b1] 52 | - @oriflame/backstage-plugin-score-card@0.5.4 53 | 54 | ## 0.4.3 55 | 56 | ### Patch Changes 57 | 58 | - d697d52: Bumped to backstage-core 1.6.0 59 | - Updated dependencies [d697d52] 60 | - @oriflame/backstage-plugin-score-card@0.5.1 61 | 62 | ## 0.4.2 63 | 64 | ### Patch Changes 65 | 66 | - Updated dependencies [a94816d] 67 | - @oriflame/backstage-plugin-score-card@0.5.0 68 | 69 | ## 0.4.1 70 | 71 | ### Patch Changes 72 | 73 | - ac344f7: added e2e tests for scorecard plugin 74 | - Updated dependencies [a4d022f] 75 | - @oriflame/backstage-plugin-score-card@0.4.1 76 | 77 | ## 0.4.0 78 | 79 | Initial version for plugins app host. 80 | -------------------------------------------------------------------------------- /packages/app/e2e-tests/entityScores.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame (Based on https://github.com/RoadieHQ/roadie-backstage-plugins source copyrighted by Larder Software Limited) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { test, expect } from '@playwright/test'; 17 | import { failOnBrowserErrors } from '@backstage/e2e-test-utils/playwright'; 18 | 19 | failOnBrowserErrors(); 20 | 21 | test('Entity Score board: displays the score board based on sample data', async ({ 22 | page, 23 | }) => { 24 | page.on('load', p => { 25 | p.evaluate(() => 26 | window.localStorage.setItem( 27 | '@backstage/core:SignInPage:provider', 28 | 'guest', 29 | ), 30 | ); 31 | }); 32 | 33 | await page.goto( 34 | '/catalog/default/component/sample-service-entity-source/score', 35 | ); 36 | 37 | await expect(page.getByText('Scoring')).toBeVisible(); 38 | await expect(page.getByText('Total score: Yellow')).toBeVisible(); 39 | 40 | await expect( 41 | page.getByRole('cell', { name: 'Area: Code Green' }), 42 | ).toBeVisible(); 43 | await expect( 44 | page.getByRole('cell', { name: 'Area: Documentation Red' }), 45 | ).toBeVisible(); 46 | await expect( 47 | page.getByRole('cell', { name: 'Area: Operations Yellow' }), 48 | ).toBeVisible(); 49 | await expect( 50 | page.getByRole('cell', { name: 'Area: Quality Red' }), 51 | ).toBeVisible(); 52 | 53 | await page 54 | .getByRole('cell', { name: 'Area: Code Green' }) 55 | .getByRole('button') 56 | .click(); 57 | await expect(page.getByText('hints: Gitflow: 100%')).toBeVisible(); 58 | 59 | const gitflowLinkHref = await page 60 | .getByRole('link', { name: 'GitFlow' }) 61 | .getAttribute('href'); 62 | 63 | expect(gitflowLinkHref).toEqual('https://link-to-wiki/2157'); 64 | }); 65 | -------------------------------------------------------------------------------- /packages/app/e2e-tests/scoreboard.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame (Based on https://github.com/RoadieHQ/roadie-backstage-plugins source copyrighted by Larder Software Limited) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { test, expect } from '@playwright/test'; 17 | import { failOnBrowserErrors } from '@backstage/e2e-test-utils/playwright'; 18 | 19 | failOnBrowserErrors(); 20 | 21 | test('Score board: displays the score board based on sample data', async ({ page }) => { 22 | // cy.loginAsGuest(); 23 | page.on('load', p => { 24 | p.evaluate(() => window.localStorage.setItem('@backstage/core:SignInPage:provider', 'guest')) 25 | }); 26 | // cy.visit('/score-board', { 27 | await page.goto('/score-board'); 28 | 29 | 30 | // onBeforeLoad(win) { 31 | // cy.stub(win.console, `log`).callsFake(msg => { 32 | // // log to Terminal 33 | // cy.task("log", msg); 34 | // }); 35 | // } 36 | // }); 37 | 38 | // cy.contains('Custom page title').should('be.visible'); 39 | await expect(page.getByRole('heading', { name: 'Custom page title' })).toBeVisible(); 40 | // cy.contains('Custom sub title').should('be.visible'); 41 | await expect(page.getByText('Custom sub title')).toBeVisible(); 42 | // cy.contains('Custom table title').should('be.visible'); 43 | await expect(page.getByRole('heading', { name: 'Custom table title' })).toBeVisible(); 44 | // cy.get('span:contains("1-4 of 4")').should('be.visible'); // beware, there is also a hidden

element 45 | await expect(await page.locator('span').filter({ hasText: '1-4 of' })).toBeVisible(); 46 | // cy.contains('audio-playback').should('be.visible'); 47 | await expect(await page.getByRole('link', { name: 'audio-playback' })).toBeVisible(); 48 | // cy.contains('team-c').should('be.visible'); 49 | await expect(page.getByRole('link', { name: 'team-c' }).first()).toBeVisible(); 50 | // cy.contains('non-valid-system').should('be.visible'); 51 | // cy.contains('Name').should('be.visible'); 52 | ['Name', 'Date', 'Code', 'Documentation', 'Operations', 'Quality', 'Security', 'Total'].forEach(async columnName => { 53 | await expect(page.getByRole('columnheader', { name: columnName })).toBeVisible(); 54 | }); 55 | 56 | // cy.contains('50 %').should('be.visible'); 57 | page.getByRole('cell', { name: '50 %' }) 58 | // cy.contains('75 %').should('be.visible'); 59 | page.getByRole('cell', { name: '75 %' }) 60 | // cy.checkForErrors(); 61 | // cy.screenshot({ capture: 'viewport' }); 62 | 63 | // cy.log('navigating to score card detail for audio-playback'); 64 | // console.log('navigating to score card detail for audio-playback'); 65 | 66 | // cy.get('a[data-id="audio-playback"]').should('be.visible').click(); 67 | await page.getByRole('link', { name: 'audio-playback' }).click(); 68 | // cy.url().should( 69 | // 'include', 70 | // '/catalog/default/system/audio-playback/score', 71 | // ); 72 | expect(page.context().pages()[0].url()).toEqual('http://localhost:3000/catalog/default/system/audio-playback/score'); 73 | // cy.contains('Scoring').should('be.visible'); 74 | await expect(page.getByText('Scoring')).toBeVisible(); 75 | // cy.contains('Total score: 57 %').should('be.visible'); 76 | await expect(page.getByText('Total score: 57 %')).toBeVisible(); 77 | // cy.contains('Code').should('be.visible'); 78 | // cy.contains('90 %').should('be.visible'); 79 | await expect(page.getByRole('cell', { name: 'Area: Code 90 %' })).toBeVisible(); 80 | // cy.contains('Documentation').should('be.visible'); 81 | // cy.contains('75 %').should('be.visible'); 82 | await expect(page.getByRole('cell', { name: 'Area: Documentation 75 %' })).toBeVisible(); 83 | // cy.contains('Operations').should('be.visible'); 84 | // cy.contains('50 %').should('be.visible'); 85 | await expect(page.getByRole('cell', { name: 'Area: Operations 50 %' })).toBeVisible(); 86 | // cy.contains('Quality').should('be.visible'); 87 | // cy.contains('25 %').should('be.visible'); 88 | await expect(page.getByRole('cell', { name: 'Area: Quality 25 %' })).toBeVisible(); 89 | // cy.contains('Security'); 90 | // cy.contains('10 %').should('be.visible'); 91 | await expect(page.getByRole('cell', { name: 'Area: Security 10 %' })).toBeVisible(); 92 | // cy.checkForErrors(); 93 | // cy.screenshot({ capture: 'viewport' }); 94 | 95 | // cy.log( 96 | // 'Clicking on button [>] that is first child of the element (td) with value=Code', 97 | // ); 98 | // cy.get('[value="Code"] > button:first-child').click(); 99 | await page.getByRole('cell', { name: 'Area: Code 90 %' }).getByRole('button').click(); 100 | // cy.contains('hints: Gitflow: 100%').should('be.visible'); 101 | await expect(page.getByText('hints: Gitflow: 100%')).toBeVisible(); 102 | // cy.get('a[data-id="2157"]') 103 | // .should('be.visible') 104 | // .should('have.attr', 'href', 'https://link-to-wiki/2157'); 105 | const gitflowLinkHref = await page.getByRole('link', { name: 'GitFlow' }).getAttribute('href'); 106 | await expect(gitflowLinkHref).toEqual('https://link-to-wiki/2157'); 107 | // cy.checkForErrors(); 108 | // cy.screenshot({ capture: 'viewport' }); 109 | 110 | }); 111 | -------------------------------------------------------------------------------- /packages/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.5.1", 4 | "private": true, 5 | "bundled": true, 6 | "backstage": { 7 | "role": "frontend" 8 | }, 9 | "scripts": { 10 | "start": "backstage-cli package start", 11 | "build": "backstage-cli package build", 12 | "clean": "backstage-cli package clean", 13 | "test": "backstage-cli package test", 14 | "lint": "backstage-cli package lint" 15 | }, 16 | "dependencies": { 17 | "@backstage/app-defaults": "^1.4.7", 18 | "@backstage/catalog-model": "^1.4.3", 19 | "@backstage/cli": "^0.25.1", 20 | "@backstage/config": "^1.1.1", 21 | "@backstage/core-app-api": "^1.11.3", 22 | "@backstage/core-components": "^0.13.10", 23 | "@backstage/core-plugin-api": "^1.8.2", 24 | "@backstage/integration-react": "^1.1.23", 25 | "@backstage/plugin-api-docs": "^0.10.3", 26 | "@backstage/plugin-catalog": "^1.16.1", 27 | "@backstage/plugin-catalog-common": "^1.0.20", 28 | "@backstage/plugin-catalog-graph": "^0.3.3", 29 | "@backstage/plugin-catalog-import": "^0.10.5", 30 | "@backstage/plugin-catalog-react": "^1.9.3", 31 | "@backstage/plugin-home": "^0.6.1", 32 | "@backstage/plugin-org": "^0.6.19", 33 | "@backstage/plugin-scaffolder": "^1.17.1", 34 | "@backstage/plugin-search": "^1.4.5", 35 | "@backstage/plugin-tech-radar": "^0.6.12", 36 | "@backstage/plugin-user-settings": "^0.8.0", 37 | "@backstage/test-utils": "^1.4.7", 38 | "@backstage/theme": "^0.5.0", 39 | "@material-ui/core": "4.12.4", 40 | "@material-ui/icons": "4.11.3", 41 | "@oriflame/backstage-plugin-score-card": "^0.9.0", 42 | "history": "5.3.0", 43 | "prop-types": "15.8.1", 44 | "react": "^18.0.0", 45 | "react-dom": "^18.0.0", 46 | "react-hot-loader": "4.13.1", 47 | "react-router": "6.26.2", 48 | "react-router-dom": "6.26.2", 49 | "react-use": "17.5.0" 50 | }, 51 | "devDependencies": { 52 | "@backstage/e2e-test-utils": "^0.1.0", 53 | "@backstage/test-utils": "^1.4.7", 54 | "@playwright/test": "^1.43.0", 55 | "@testing-library/dom": "^9.0.0", 56 | "@testing-library/jest-dom": "^6.0.0", 57 | "@testing-library/react": "^14.0.0", 58 | "@testing-library/user-event": "^14.0.0", 59 | "@types/jest": "*", 60 | "@types/node": "^20.11.5", 61 | "@types/react-dom": "*", 62 | "cross-env": "7.0.3", 63 | "start-server-and-test": "1.14.0" 64 | }, 65 | "browserslist": { 66 | "production": [ 67 | ">0.2%", 68 | "not dead", 69 | "not op_mini all" 70 | ], 71 | "development": [ 72 | "last 1 chrome version", 73 | "last 1 firefox version", 74 | "last 1 safari version" 75 | ] 76 | }, 77 | "files": [ 78 | "dist" 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /packages/app/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oriflame/backstage-plugins/143e210c8c87fca3c9f039d75757141b7d9e264d/packages/app/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /packages/app/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oriflame/backstage-plugins/143e210c8c87fca3c9f039d75757141b7d9e264d/packages/app/public/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/app/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oriflame/backstage-plugins/143e210c8c87fca3c9f039d75757141b7d9e264d/packages/app/public/favicon-16x16.png -------------------------------------------------------------------------------- /packages/app/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oriflame/backstage-plugins/143e210c8c87fca3c9f039d75757141b7d9e264d/packages/app/public/favicon-32x32.png -------------------------------------------------------------------------------- /packages/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oriflame/backstage-plugins/143e210c8c87fca3c9f039d75757141b7d9e264d/packages/app/public/favicon.ico -------------------------------------------------------------------------------- /packages/app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 16 | 21 | 22 | 23 | 28 | 34 | 40 | 45 | <%= config.getString('app.title') %> 46 | 47 | <% if (config.has('app.googleAnalyticsTrackingId')) { %> 48 | 52 | 64 | <% } %> <% if (config.has('app.datadogRum')) { %> 65 | 98 | <% } %> 99 | 100 | 101 | 102 | 103 |

104 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /packages/app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Backstage", 3 | "name": "Backstage", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "48x48", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /packages/app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /packages/app/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | Created by potrace 1.11, written by Peter Selinger 2001-2013 -------------------------------------------------------------------------------- /packages/app/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame (Based on https://github.com/RoadieHQ/roadie-backstage-plugins source copyrighted by Larder Software Limited) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | import React from 'react'; 19 | import { renderWithEffects } from '@backstage/test-utils'; 20 | import App from './App'; 21 | 22 | describe('App', () => { 23 | it('should render', async () => { 24 | process.env = { 25 | NODE_ENV: 'test', 26 | APP_CONFIG: [ 27 | { 28 | data: { 29 | app: { title: 'Test' }, 30 | backend: { baseUrl: 'http://localhost:7007' }, 31 | scorecards: { 32 | jsonDataUrl: 'http://localhost:8090/plugins/score-card/sample-data/', 33 | }, 34 | }, 35 | context: 'test', 36 | }, 37 | ] as any, 38 | }; 39 | 40 | const rendered = await renderWithEffects(); 41 | expect(rendered.baseElement).toBeInTheDocument(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/app/src/App.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame (Based on https://github.com/RoadieHQ/roadie-backstage-plugins source copyrighted by Larder Software Limited) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import { Navigate, Route } from 'react-router'; 19 | import { 20 | CatalogEntityPage, 21 | CatalogIndexPage, 22 | catalogPlugin, 23 | } from '@backstage/plugin-catalog'; 24 | import { orgPlugin } from '@backstage/plugin-org'; 25 | import { UserSettingsPage } from '@backstage/plugin-user-settings'; 26 | import { apis } from './apis'; 27 | import { entityPage } from './components/catalog/EntityPage'; 28 | import { Root } from './components/Root'; 29 | 30 | import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; 31 | import { AppRouter, FlatRoutes } from '@backstage/core-app-api'; 32 | import { createApp } from '@backstage/app-defaults'; 33 | import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card'; 34 | 35 | const app = createApp({ 36 | apis, 37 | bindRoutes({ bind }) { 38 | bind(orgPlugin.externalRoutes, { 39 | catalogIndex: catalogPlugin.routes.catalogIndex, 40 | }); 41 | }, 42 | }); 43 | 44 | const routes = ( 45 | 46 | } /> 47 | } /> 48 | } 51 | > 52 | {entityPage} 53 | 54 | } /> 55 | 56 | 64 | } 65 | /> 66 | 67 | ); 68 | 69 | export default app.createRoot( 70 | <> 71 | 72 | 73 | 74 | {routes} 75 | 76 | , 77 | ); 78 | -------------------------------------------------------------------------------- /packages/app/src/apis.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame (Based on https://github.com/RoadieHQ/roadie-backstage-plugins source copyrighted by Larder Software Limited) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | import { 19 | ScmIntegrationsApi, 20 | scmIntegrationsApiRef, 21 | ScmAuth, 22 | } from '@backstage/integration-react'; 23 | import { 24 | AnyApiFactory, 25 | configApiRef, 26 | createApiFactory, 27 | } from '@backstage/core-plugin-api'; 28 | 29 | export const apis: AnyApiFactory[] = [ 30 | createApiFactory({ 31 | api: scmIntegrationsApiRef, 32 | deps: { configApi: configApiRef }, 33 | factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), 34 | }), 35 | ScmAuth.createDefaultApiFactory(), 36 | ]; 37 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/LogoIcon.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // eslint-disable-next-line notice/notice 18 | import React from 'react'; 19 | import { makeStyles } from '@material-ui/core'; 20 | 21 | const useStyles = makeStyles({ 22 | svg: { 23 | width: 'auto', 24 | height: 28, 25 | }, 26 | path: { 27 | fill: '#7df3e1', 28 | }, 29 | }); 30 | 31 | const LogoIcon = () => { 32 | const classes = useStyles(); 33 | 34 | return ( 35 | 40 | 44 | 45 | ); 46 | }; 47 | 48 | export default LogoIcon; 49 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/Root.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame (Based on https://github.com/RoadieHQ/roadie-backstage-plugins source copyrighted by Larder Software Limited) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | import React, { PropsWithChildren } from 'react'; 19 | import { Link, makeStyles } from '@material-ui/core'; 20 | import LibraryBooks from '@material-ui/icons/LibraryBooks'; 21 | import LogoFull from './LogoFull'; 22 | import LogoIcon from './LogoIcon'; 23 | import { NavLink } from 'react-router-dom'; 24 | import { 25 | Settings as SidebarSettings, 26 | UserSettingsSignInAvatar, 27 | } from '@backstage/plugin-user-settings'; 28 | import { 29 | Sidebar, 30 | sidebarConfig, 31 | SidebarDivider, 32 | SidebarGroup, 33 | SidebarItem, 34 | SidebarPage, 35 | useSidebarOpenState, 36 | } from '@backstage/core-components'; 37 | import Score from '@material-ui/icons/Score'; 38 | 39 | const useSidebarLogoStyles = makeStyles({ 40 | root: { 41 | width: sidebarConfig.drawerWidthClosed, 42 | height: 3 * sidebarConfig.logoHeight, 43 | display: 'flex', 44 | flexFlow: 'row nowrap', 45 | alignItems: 'center', 46 | marginBottom: -14, 47 | }, 48 | link: { 49 | width: sidebarConfig.drawerWidthClosed, 50 | marginLeft: 24, 51 | }, 52 | }); 53 | 54 | const SidebarLogo = () => { 55 | const classes = useSidebarLogoStyles(); 56 | const { isOpen } = useSidebarOpenState(); 57 | 58 | return ( 59 |
60 | 67 | {isOpen ? : } 68 | 69 |
70 | ); 71 | }; 72 | 73 | export const Root = ({ children }: PropsWithChildren<{}>) => ( 74 | 75 | 76 | 77 | {/* */} 78 | {/* Global nav, not org-specific */} 79 | {/* */} 80 | 81 | {/* End global nav */} 82 | 83 | 84 | 85 | 86 | } 89 | to="/settings" 90 | > 91 | 92 | 93 | 94 | {children} 95 | 96 | ); 97 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // eslint-disable-next-line notice/notice 18 | export { Root } from './Root'; 19 | -------------------------------------------------------------------------------- /packages/app/src/components/home/HomePage.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame (Based on https://github.com/RoadieHQ/roadie-backstage-plugins source copyrighted by Larder Software Limited) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | import React from 'react'; 19 | import Grid from '@material-ui/core/Grid'; 20 | import { HomePageToolkit } from '@backstage/plugin-home'; 21 | import { Content, PageWithHeader } from '@backstage/core-components'; 22 | 23 | export const HomePage = () => { 24 | return ( 25 | 26 | 27 | 28 | 29 | , 35 | }, 36 | ]} 37 | /> 38 | 39 | 40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /packages/app/src/index.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line notice/notice 2 | import '@backstage/cli/asset-types'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom/client'; 5 | import App from './App'; 6 | 7 | ReactDOM.createRoot(document.getElementById('root')!).render(); 8 | -------------------------------------------------------------------------------- /packages/app/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line notice/notice 2 | import '@testing-library/jest-dom'; 3 | -------------------------------------------------------------------------------- /packages/backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 2 | -------------------------------------------------------------------------------- /packages/backend/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # backend 2 | 3 | ## 1.1.0 4 | 5 | ### Minor Changes 6 | 7 | - 32cc2fb: Upgraded to Backstage core 1.22.1 8 | 9 | ## 1.0.17 10 | 11 | ### Patch Changes 12 | 13 | - e8c6214: Bumped backstage core libs from 1.8.3 to 1.10.1 14 | - Updated dependencies [e8c6214] 15 | - app@0.4.6 16 | 17 | ## 1.0.16 18 | 19 | ### Patch Changes 20 | 21 | - 398f28d: bump version for backstage core components to 1.8.3 22 | - Updated dependencies [398f28d] 23 | - app@0.4.5 24 | 25 | ## 1.0.15 26 | 27 | ### Patch Changes 28 | 29 | - ac868b1: bumped to 1.7.0 backstage core version 30 | - Updated dependencies [ac868b1] 31 | - app@0.4.4 32 | 33 | ## 1.0.14 34 | 35 | ### Patch Changes 36 | 37 | - d697d52: Bumped to backstage-core 1.6.0 38 | - Updated dependencies [d697d52] 39 | - app@0.4.3 40 | 41 | ## 0.4.0 42 | 43 | Initial version for plugins app host. 44 | -------------------------------------------------------------------------------- /packages/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # This dockerfile builds an image for the backend package. 2 | # It should be executed with the root of the repo as docker context. 3 | # 4 | # Before building this image, be sure to have run the following commands in the repo root: 5 | # 6 | # yarn install 7 | # yarn tsc 8 | # yarn build:backend 9 | # 10 | # Once the commands have been run, you can build the image using `yarn build-image` 11 | 12 | FROM node:18-bookworm-slim 13 | 14 | # Install sqlite3 dependencies. You can skip this if you don't use sqlite3 in the image, 15 | # in which case you should also move better-sqlite3 to "devDependencies" in package.json. 16 | # Additionally, we install dependencies for `techdocs.generator.runIn: local`. 17 | # https://backstage.io/docs/features/techdocs/getting-started#disabling-docker-in-docker-situation-optional 18 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ 19 | --mount=type=cache,target=/var/lib/apt,sharing=locked \ 20 | apt-get update && \ 21 | apt-get install -y --no-install-recommends libsqlite3-dev python3 python3-pip python3-venv build-essential && \ 22 | yarn config set python /usr/bin/python3 23 | 24 | # Set up a virtual environment for mkdocs-techdocs-core. 25 | ENV VIRTUAL_ENV=/opt/venv 26 | RUN python3 -m venv $VIRTUAL_ENV 27 | ENV PATH="$VIRTUAL_ENV/bin:$PATH" 28 | 29 | RUN pip3 install mkdocs-techdocs-core==1.1.7 30 | 31 | # From here on we use the least-privileged `node` user to run the backend. 32 | WORKDIR /app 33 | RUN chown node:node /app 34 | USER node 35 | 36 | 37 | # This switches many Node.js dependencies to production mode. 38 | ENV NODE_ENV production 39 | 40 | # Copy over Yarn 3 configuration, release, and plugins 41 | COPY --chown=node:node .yarn ./.yarn 42 | COPY --chown=node:node .yarnrc.yml ./ 43 | 44 | # Copy repo skeleton first, to avoid unnecessary docker cache invalidation. 45 | # The skeleton contains the package.json of each package in the monorepo, 46 | # and along with yarn.lock and the root package.json, that's enough to run yarn install. 47 | COPY --chown=node:node yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ 48 | RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz 49 | 50 | RUN --mount=type=cache,target=/home/node/.yarn/berry/cache,sharing=locked,uid=1000,gid=1000 \ 51 | yarn workspaces focus --all --production 52 | 53 | # Then copy the rest of the backend bundle, along with any other files we might want. 54 | COPY --chown=node:node packages/backend/dist/bundle.tar.gz app-config*.yaml ./ 55 | RUN tar xzf bundle.tar.gz && rm bundle.tar.gz 56 | 57 | CMD ["node", "packages/backend", "--config", "app-config.yaml"] 58 | -------------------------------------------------------------------------------- /packages/backend/README.md: -------------------------------------------------------------------------------- 1 | # example-backend 2 | 3 | This package is an EXAMPLE of a Backstage backend. 4 | 5 | The main purpose of this package is to provide a test bed for Backstage plugins 6 | that have a backend part. Feel free to experiment locally or within your fork by 7 | adding dependencies and routes to this backend, to try things out. 8 | 9 | Our goal is to eventually amend the create-app flow of the CLI, such that a 10 | production ready version of a backend skeleton is made alongside the frontend 11 | app. Until then, feel free to experiment here! 12 | 13 | ## Development 14 | 15 | To run the example backend, first go to the project root and run 16 | 17 | ```bash 18 | yarn install 19 | yarn tsc 20 | yarn build 21 | ``` 22 | 23 | You should only need to do this once. 24 | 25 | After that, go to the `packages/backend` directory and run 26 | 27 | ```bash 28 | AUTH_GOOGLE_CLIENT_ID=x AUTH_GOOGLE_CLIENT_SECRET=x \ 29 | AUTH_GITHUB_CLIENT_ID=x AUTH_GITHUB_CLIENT_SECRET=x \ 30 | AUTH_OAUTH2_CLIENT_ID=x AUTH_OAUTH2_CLIENT_SECRET=x \ 31 | AUTH_OAUTH2_AUTH_URL=x AUTH_OAUTH2_TOKEN_URL=x \ 32 | LOG_LEVEL=debug \ 33 | yarn start 34 | ``` 35 | 36 | Substitute `x` for actual values, or leave them as dummy values just to try out 37 | the backend without using the auth or sentry features. 38 | 39 | The backend starts up on port 7007 per default. 40 | 41 | ## Populating The Catalog 42 | 43 | If you want to use the catalog functionality, you need to add so called 44 | locations to the backend. These are places where the backend can find some 45 | entity descriptor data to consume and serve. For more information, see 46 | [Software Catalog Overview - Adding Components to the Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview#adding-components-to-the-catalog). 47 | 48 | To get started quickly, this template already includes some statically configured example locations 49 | in `app-config.yaml` under `catalog.locations`. You can remove and replace these locations as you 50 | like, and also override them for local development in `app-config.local.yaml`. 51 | 52 | ## Authentication 53 | 54 | We chose [Passport](http://www.passportjs.org/) as authentication platform due 55 | to its comprehensive set of supported authentication 56 | [strategies](http://www.passportjs.org/packages/). 57 | 58 | Read more about the 59 | [auth-backend](https://github.com/backstage/backstage/blob/master/plugins/auth-backend/README.md) 60 | and 61 | [how to add a new provider](https://github.com/backstage/backstage/blob/master/docs/auth/add-auth-provider.md) 62 | 63 | ## Documentation 64 | 65 | - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) 66 | - [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) 67 | -------------------------------------------------------------------------------- /packages/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.1.0", 4 | "main": "dist/index.cjs.js", 5 | "types": "src/index.ts", 6 | "private": true, 7 | "backstage": { 8 | "role": "backend" 9 | }, 10 | "scripts": { 11 | "start": "backstage-cli package start", 12 | "build": "backstage-cli package build", 13 | "lint": "backstage-cli package lint", 14 | "test": "backstage-cli package test", 15 | "clean": "backstage-cli package clean", 16 | "build-image": "docker build ../.. -f Dockerfile --tag backstage", 17 | "migrate:create": "knex migrate:make -x ts" 18 | }, 19 | "dependencies": { 20 | "@backstage/backend-common": "^0.20.2", 21 | "@backstage/backend-tasks": "^0.5.14", 22 | "@backstage/catalog-client": "^1.5.2", 23 | "@backstage/catalog-model": "^1.4.3", 24 | "@backstage/config": "^1.1.1", 25 | "@backstage/integration": "^1.8.0", 26 | "@backstage/plugin-app-backend": "^0.3.57", 27 | "@backstage/plugin-auth-backend": "^0.20.3", 28 | "@backstage/plugin-catalog-backend": "^1.16.1", 29 | "@backstage/plugin-permission-common": "^0.7.12", 30 | "@backstage/plugin-permission-node": "^0.7.20", 31 | "@backstage/plugin-proxy-backend": "^0.4.7", 32 | "@backstage/plugin-scaffolder-backend": "^1.20.0", 33 | "@gitbeaker/node": "35.8.1", 34 | "@octokit/rest": "18.12.0", 35 | "app": "link:../app", 36 | "better-sqlite3": "9.6.0", 37 | "dockerode": "3.3.5", 38 | "express": "4.19.2", 39 | "express-promise-router": "4.1.1", 40 | "knex": "2.5.1", 41 | "luxon": "3.4.4", 42 | "node-gyp": "^9.0.0", 43 | "serialize-error": "8.1.0", 44 | "winston": "3.13.0" 45 | }, 46 | "devDependencies": { 47 | "@backstage/cli": "^0.25.1", 48 | "@types/dockerode": "3.3.28", 49 | "@types/express": "4.17.21", 50 | "@types/express-serve-static-core": "4.19.0", 51 | "@types/luxon": "3.4.2" 52 | }, 53 | "files": [ 54 | "dist" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /packages/backend/src/index.test.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line notice/notice 2 | import { PluginEnvironment } from './types'; 3 | 4 | describe('test', () => { 5 | it('unbreaks the test runner', () => { 6 | const unbreaker = {} as PluginEnvironment; 7 | expect(unbreaker).toBeTruthy(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/backend/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Hi! 3 | * 4 | * Note that this is an EXAMPLE Backstage backend. Please check the README. 5 | * 6 | * Happy hacking! 7 | */ 8 | 9 | // eslint-disable-next-line notice/notice 10 | const { version } = require('../package.json'); 11 | 12 | import Router from 'express-promise-router'; 13 | import { 14 | CacheManager, 15 | createServiceBuilder, 16 | getRootLogger, 17 | loadBackendConfig, 18 | notFoundHandler, 19 | DatabaseManager, 20 | SingleHostDiscovery, 21 | UrlReaders, 22 | useHotMemoize, 23 | ServerTokenManager, 24 | } from '@backstage/backend-common'; 25 | import { Config } from '@backstage/config'; 26 | import healthcheck from './plugins/healthcheck'; 27 | import { TaskScheduler } from '@backstage/backend-tasks'; 28 | import app from './plugins/app'; 29 | import auth from './plugins/auth'; 30 | import catalog from './plugins/catalog'; 31 | import scaffolder from './plugins/scaffolder'; 32 | import proxy from './plugins/proxy'; 33 | import { PluginEnvironment } from './types'; 34 | import { ServerPermissionClient } from '@backstage/plugin-permission-node'; 35 | import {serializeError} from 'serialize-error'; 36 | 37 | const safeFatalError = async (message: string, error?: Error) => { 38 | try { 39 | // if available, use the root logger (shall send the message to AppInsights too) 40 | const logger = getRootLogger(); 41 | logger.error(message); 42 | logger.error(`Full error: ${serializeError(error)}`) 43 | } catch (_e) { 44 | console.error(message); 45 | } 46 | if (process.env.NODE_ENV === 'development') { 47 | console.error( 48 | `DEVELOPMENT server (watcher) ${process.ppid} will be killed now...`, 49 | ); 50 | process.kill(process.ppid); 51 | } 52 | console.info(`Process ${process.pid} will exit now...`); 53 | process.exit(1); 54 | }; 55 | 56 | function makeCreateEnv(config: Config) { 57 | const root = getRootLogger(); 58 | const reader = UrlReaders.default({ logger: root, config }); 59 | const discovery = SingleHostDiscovery.fromConfig(config); 60 | const tokenManager = ServerTokenManager.fromConfig(config, { logger: root }); 61 | const permissions = ServerPermissionClient.fromConfig(config, { 62 | discovery, 63 | tokenManager, 64 | }); 65 | const databaseManager = DatabaseManager.fromConfig(config); 66 | const cacheManager = CacheManager.fromConfig(config); 67 | const taskScheduler = TaskScheduler.fromConfig(config); 68 | 69 | root.info(`Created UrlReader ${reader}`); 70 | 71 | return (plugin: string): PluginEnvironment => { 72 | const logger = root.child({ type: 'plugin', plugin }); 73 | const database = databaseManager.forPlugin(plugin); 74 | const cache = cacheManager.forPlugin(plugin); 75 | const scheduler = taskScheduler.forPlugin(plugin); 76 | return { 77 | logger, 78 | cache, 79 | database, 80 | config, 81 | reader, 82 | discovery, 83 | tokenManager, 84 | permissions, 85 | scheduler, 86 | }; 87 | }; 88 | } 89 | 90 | async function main() { 91 | const logger = getRootLogger(); 92 | 93 | logger.info(`App starting: version [${version}]`); 94 | 95 | const config = await loadBackendConfig({ 96 | argv: process.argv, 97 | logger, 98 | }); 99 | const createEnv = makeCreateEnv(config); 100 | 101 | const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck')); 102 | const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); 103 | const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder')); 104 | const authEnv = useHotMemoize(module, () => createEnv('auth')); 105 | const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); 106 | const appEnv = useHotMemoize(module, () => createEnv('app')); 107 | 108 | const apiRouter = Router(); 109 | apiRouter.use('/catalog', await catalog(catalogEnv)); 110 | apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); 111 | apiRouter.use('/auth', await auth(authEnv)); 112 | apiRouter.use('/proxy', await proxy(proxyEnv)); 113 | apiRouter.use(notFoundHandler()); 114 | 115 | try { 116 | logger.info('Creating service...'); 117 | const service = createServiceBuilder(module) 118 | .loadConfig(config) 119 | .addRouter('', await healthcheck(healthcheckEnv)) 120 | .addRouter('/api', apiRouter) 121 | .addRouter('', await app(appEnv)); 122 | 123 | logger.info('Starting service...'); 124 | await service.start().catch(async err => { 125 | await safeFatalError(`service.start() failed: ${err}`); 126 | }); 127 | } catch (error) { 128 | if (typeof error === 'string') { 129 | await safeFatalError(`main() failed: ${error}`); 130 | } else if (error instanceof Error) { 131 | await safeFatalError(`main() failed: ${error.message}`, error as Error); 132 | } 133 | } 134 | } 135 | 136 | module.hot?.accept(); 137 | main().catch(async error => { 138 | await safeFatalError(`Backend failed to start up: ${error}`); 139 | }); 140 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/app.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable notice/notice */ 2 | import { createRouter } from '@backstage/plugin-app-backend'; 3 | import { Router } from 'express'; 4 | import { PluginEnvironment } from '../types'; 5 | 6 | export default async function createPlugin({ 7 | logger, 8 | config, 9 | }: PluginEnvironment): Promise { 10 | return await createRouter({ 11 | logger, 12 | config, 13 | appPackageName: 'app', 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/auth.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line notice/notice 2 | import { createRouter } from '@backstage/plugin-auth-backend'; 3 | import { Router } from 'express'; 4 | import { PluginEnvironment } from '../types'; 5 | 6 | export default async function createPlugin({ 7 | logger, 8 | database, 9 | config, 10 | discovery, 11 | tokenManager, 12 | }: PluginEnvironment): Promise { 13 | return await createRouter({ 14 | logger, 15 | config, 16 | database, 17 | discovery, 18 | tokenManager, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/catalog.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line notice/notice 2 | import { 3 | CatalogBuilder, 4 | EntityProvider, 5 | } from '@backstage/plugin-catalog-backend'; 6 | import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; 7 | import { Router } from 'express'; 8 | import { PluginEnvironment } from '../types'; 9 | import { Duration } from 'luxon'; 10 | 11 | export default async function createPlugin( 12 | env: PluginEnvironment, 13 | ): Promise { 14 | type RunnableProvider = EntityProvider & { 15 | run: () => Promise; 16 | }; 17 | const builder = await CatalogBuilder.create(env); 18 | const providers: RunnableProvider[] = []; 19 | 20 | 21 | builder.addProcessor(new ScaffolderEntitiesProcessor()); 22 | 23 | const { processingEngine, router } = await builder.build(); 24 | await processingEngine.start(); 25 | 26 | for (const [i, provider] of providers.entries()) { 27 | env.logger.info(`configuring ${provider.getProviderName()} schedule`); 28 | await env.scheduler.scheduleTask({ 29 | id: `run_${provider.getProviderName()}_${i}`, 30 | fn: async () => { 31 | await provider.run(); 32 | }, 33 | frequency: Duration.fromObject({ minutes: 5 }), 34 | timeout: Duration.fromObject({ minutes: 10 }), 35 | }); 36 | } 37 | return router; 38 | } 39 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/healthcheck.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame (Based on https://github.com/RoadieHQ/roadie-backstage-plugins source copyrighted by Larder Software Limited) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { createStatusCheckRouter } from '@backstage/backend-common'; 18 | import { Router } from 'express'; 19 | import { PluginEnvironment } from '../types'; 20 | 21 | export default async function createPlugin( 22 | env: PluginEnvironment, 23 | ): Promise { 24 | return await createStatusCheckRouter({ 25 | logger: env.logger, 26 | path: '/healthcheck', 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/proxy.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame (Based on https://github.com/RoadieHQ/roadie-backstage-plugins source copyrighted by Larder Software Limited) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { createRouter } from '@backstage/plugin-proxy-backend'; 18 | import { Router } from 'express'; 19 | import { PluginEnvironment } from '../types'; 20 | 21 | export default async function createPlugin({ 22 | logger, 23 | config, 24 | discovery, 25 | }: PluginEnvironment): Promise { 26 | return await createRouter({ logger, config, discovery }); 27 | } 28 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/scaffolder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame (Based on https://github.com/RoadieHQ/roadie-backstage-plugins source copyrighted by Larder Software Limited) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | DockerContainerRunner, 19 | SingleHostDiscovery, 20 | UrlReader, 21 | ContainerRunner, 22 | } from '@backstage/backend-common'; 23 | import { CatalogClient } from '@backstage/catalog-client'; 24 | import { 25 | createRouter, 26 | createBuiltinActions, 27 | TemplateAction, 28 | } from '@backstage/plugin-scaffolder-backend'; 29 | import Docker from 'dockerode'; 30 | import { Router } from 'express'; 31 | import type { PluginEnvironment } from '../types'; 32 | import { ScmIntegrations } from '@backstage/integration'; 33 | import { Config } from '@backstage/config'; 34 | 35 | export const createActions = (options: { 36 | reader: UrlReader; 37 | integrations: ScmIntegrations; 38 | config: Config; 39 | containerRunner: ContainerRunner; 40 | catalogClient: CatalogClient; 41 | }): TemplateAction[] => { 42 | const { reader, integrations, config, catalogClient } = options; 43 | const defaultActions = createBuiltinActions({ 44 | reader, 45 | integrations, 46 | catalogClient, 47 | config, 48 | }); 49 | 50 | return defaultActions; 51 | }; 52 | 53 | export default async function createPlugin({ 54 | logger, 55 | config, 56 | database, 57 | reader, 58 | }: PluginEnvironment): Promise { 59 | const dockerClient = new Docker(); 60 | const containerRunner = new DockerContainerRunner({ dockerClient }); 61 | 62 | const discovery = SingleHostDiscovery.fromConfig(config); 63 | const catalogClient = new CatalogClient({ discoveryApi: discovery }); 64 | 65 | return await createRouter({ 66 | logger, 67 | config, 68 | database, 69 | catalogClient, 70 | reader, 71 | actions: createActions({ 72 | reader, 73 | integrations: ScmIntegrations.fromConfig(config), 74 | config, 75 | catalogClient: catalogClient, 76 | containerRunner: containerRunner, 77 | }), 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /packages/backend/src/types.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line notice/notice 2 | import { Logger } from 'winston'; 3 | import { Config } from '@backstage/config'; 4 | import { 5 | PluginCacheManager, 6 | PluginDatabaseManager, 7 | PluginEndpointDiscovery, 8 | TokenManager, 9 | UrlReader, 10 | } from '@backstage/backend-common'; 11 | import { ServerPermissionClient } from '@backstage/plugin-permission-node'; 12 | import { PluginTaskScheduler } from '@backstage/backend-tasks'; 13 | 14 | export type PluginEnvironment = { 15 | logger: Logger; 16 | cache: PluginCacheManager; 17 | database: PluginDatabaseManager; 18 | config: Config; 19 | reader: UrlReader; 20 | discovery: PluginEndpointDiscovery; 21 | tokenManager: TokenManager; 22 | permissions: ServerPermissionClient; 23 | scheduler: PluginTaskScheduler; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/entities/publish-github-template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scaffolder.backstage.io/v1beta3 2 | kind: Template 3 | metadata: 4 | name: create-repository 5 | title: Create a github repository 6 | description: Creates a github repository with the selected settings 7 | spec: 8 | owner: roadie 9 | type: service 10 | 11 | parameters: 12 | - title: Create Repo 13 | properties: 14 | repoUrl: 15 | title: Repository Location 16 | type: string 17 | ui:field: RepoUrlPicker 18 | ui:options: 19 | requestUserCredentials: 20 | secretsKey: USER_OAUTH_TOKEN 21 | additionalScopes: 22 | github: 23 | - workflow:write 24 | allowedHosts: 25 | - github.com 26 | description: 27 | type: string 28 | defaultBranch: 29 | default: master 30 | type: string 31 | repoVisibility: 32 | type: string 33 | enum: ['private', 'public', 'internal'] 34 | requireCodeOwnerReviews: 35 | type: boolean 36 | 37 | collaborators: 38 | type: array 39 | items: 40 | type: 'object' 41 | required: ['username', 'access'] 42 | properties: 43 | access: 44 | type: 'string' 45 | description: 'The type of access for the user' 46 | enum: ['push', 'pull', 'admin', 'maintain', 'triage'] 47 | username: 48 | type: 'string' 49 | description: 'The username or group' 50 | topics: 51 | type: array 52 | items: 53 | type: string 54 | 55 | steps: 56 | - id: publish 57 | name: Create repository 58 | action: publish:github 59 | input: 60 | repoUrl: ${{ parameters.repoUrl }} 61 | topics: ${{ parameters.topics }} 62 | collaborators: ${{ parameters.collaborators }} 63 | repoVisibility: ${{ parameters.repoVisibility }} 64 | defaultBranch: ${{ parameters.defaultBranch }} 65 | requireCodeOwnerReviews: ${{parameters.requireCodeOwnerReviews}} 66 | description: ${{ parameters.description }} 67 | token: ${{ secrets.USER_OAUTH_TOKEN }} 68 | 69 | output: 70 | remoteUrl: ${{steps.publish.output.remoteUrl}} 71 | -------------------------------------------------------------------------------- /packages/entities/s3-upload-template.yaml: -------------------------------------------------------------------------------- 1 | # Notice the v1beta3 version 2 | apiVersion: scaffolder.backstage.io/v1beta3 3 | kind: Template 4 | metadata: 5 | name: s3-upload 6 | title: scaffolder-backend-module-aws s3 upload template 7 | description: An example template to test most of the scaffolder-backend-module-utils actions 8 | spec: 9 | owner: roadie 10 | type: service 11 | 12 | parameters: 13 | - title: Create File 14 | properties: 15 | createPath: 16 | title: Path 17 | type: string 18 | description: workspace path to zip 19 | createContent: 20 | title: File Content 21 | type: string 22 | description: content of the file 23 | ui:widget: textarea 24 | ui:options: 25 | rows: 5 26 | 27 | steps: 28 | - id: createFile 29 | name: Create File 30 | action: roadiehq:utils:fs:write 31 | input: 32 | path: ${{ parameters.createPath }} 33 | content: ${{ parameters.createContent }} 34 | 35 | - id: s3Upload 36 | name: Upload to S3 37 | action: roadiehq:aws:s3:cp 38 | input: 39 | bucket: scaffolder-actions-upload-test 40 | region: eu-west-1 41 | -------------------------------------------------------------------------------- /packages/entities/test-entity.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: sample-service 5 | description: | 6 | A service for testing Backstage functionality. Configured for GitHub Actions, Sentry, AWS Lambda. 7 | annotations: 8 | github.com/project-slug: organisation/github-project-slug 9 | aws.com/lambda-function-name: HelloWorld 10 | aws.com/lambda-region: eu-west-1 11 | jira/project-key: TEST 12 | jira/component: COMP 13 | datadoghq.com/dashboard-url: https://p.datadoghq.eu/sb/test-datadog-link 14 | datadoghq.com/graph-token: qwertytoken1234 15 | argocd/app-name: test-app 16 | bugsnag.com/project-key: RoadieHQTest/qwerty12345 17 | spec: 18 | type: service 19 | owner: roadie 20 | lifecycle: experimental 21 | providesApis: 22 | - sample-service 23 | --- 24 | apiVersion: backstage.io/v1alpha1 25 | kind: Component 26 | metadata: 27 | name: sample-service-github 28 | description: | 29 | A service for testing Backstage functionality. Configured for GitHub Actions, Sentry, AWS Lambda. 30 | annotations: 31 | github.com/project-slug: RoadieHQ/roadie-backstage-plugins 32 | aws.com/lambda-function-name: HelloWorld 33 | aws.com/lambda-region: eu-west-1 34 | jira/project-key: TEST 35 | jira/component: COMP 36 | datadoghq.com/dashboard-url: https://p.datadoghq.eu/sb/test-datadog-link 37 | datadoghq.com/graph-token: qwertytoken1234 38 | argocd/app-name: test-app 39 | bugsnag.com/project-key: RoadieHQTest/qwerty12345 40 | spec: 41 | type: service 42 | owner: roadie 43 | lifecycle: experimental 44 | providesApis: 45 | - sample-service 46 | 47 | --- 48 | apiVersion: backstage.io/v1alpha1 49 | kind: Component 50 | metadata: 51 | name: sample-service-2 52 | description: | 53 | A service for testing Travis CI plugin 54 | annotations: 55 | travis-ci.com/repo-slug: RoadieHQ/sample-service 56 | spec: 57 | type: service 58 | owner: roadie 59 | lifecycle: experimental 60 | providesApis: 61 | - sample-service-2 62 | 63 | --- 64 | apiVersion: backstage.io/v1alpha1 65 | kind: Component 66 | metadata: 67 | name: sample-service-3 68 | description: | 69 | A service for testing Buildkite and Prometheus plugins 70 | annotations: 71 | buildkite.com/project-slug: exampleorganization/exampleproject 72 | prometheus.io/alert: 'test alert name' 73 | prometheus.io/rule: node_memory_Active_bytes|instance,memUsage 74 | 75 | spec: 76 | type: service 77 | owner: roadie 78 | lifecycle: experimental 79 | providesApis: 80 | - sample-service-3 81 | --- 82 | apiVersion: backstage.io/v1alpha1 83 | kind: Component 84 | metadata: 85 | name: sample-service-entity-source 86 | description: | 87 | A service for testing the annotation location 88 | annotations: 89 | scorecard/jsonDataUrl: http://localhost:8090/plugins/score-card/sample-data/custom-annotation-location/service.json 90 | 91 | spec: 92 | type: service 93 | owner: roadie 94 | lifecycle: experimental 95 | providesApis: 96 | - sample-service-3 97 | --- 98 | apiVersion: backstage.io/v1alpha1 99 | kind: System 100 | metadata: 101 | name: sample-system-entity-source 102 | description: | 103 | A system for testing the annotation location 104 | annotations: 105 | scorecard/jsonDataUrl: http://localhost:8090/plugins/score-card/sample-data/custom-annotation-location/system.json 106 | 107 | spec: 108 | owner: roadie 109 | lifecycle: experimental 110 | -------------------------------------------------------------------------------- /packages/entities/test-template.yaml: -------------------------------------------------------------------------------- 1 | # Notice the v1beta3 version 2 | apiVersion: scaffolder.backstage.io/v1beta3 3 | kind: Template 4 | # some metadata about the template itself 5 | metadata: 6 | name: zip-demo 7 | title: scaffolder-backend-module-utils test template 8 | description: An example template to test most of the scaffolder-backend-module-utils actions 9 | spec: 10 | owner: roadie 11 | type: service 12 | 13 | parameters: 14 | - title: Create File 15 | properties: 16 | createPath: 17 | title: Path 18 | type: string 19 | description: workspace path to zip 20 | createContent: 21 | title: File Content 22 | type: string 23 | description: content of the file 24 | ui:widget: textarea 25 | ui:options: 26 | rows: 5 27 | 28 | - title: Sleep 29 | properties: 30 | amount: 31 | title: Sleep 32 | type: number 33 | description: Amount to sleep in seconds 34 | 35 | - title: Append 36 | properties: 37 | appendPath: 38 | title: Path 39 | type: string 40 | description: Amount to sleep in seconds 41 | appendContent: 42 | title: Content 43 | type: string 44 | description: This will be appended to the end of the file 45 | ui:widget: textarea 46 | ui:options: 47 | rows: 5 48 | 49 | - title: Zip 50 | properties: 51 | zipPath: 52 | title: Path 53 | type: string 54 | description: workspace path to zip 55 | 56 | zipOutputPath: 57 | title: Output Path 58 | type: string 59 | description: Name of the output zip 60 | 61 | - title: Create Repo 62 | properties: 63 | repoUrl: 64 | title: Repository Location 65 | type: string 66 | ui:field: RepoUrlPicker 67 | ui:options: 68 | requestUserCredentials: 69 | secretsKey: USER_OAUTH_TOKEN 70 | additionalScopes: 71 | github: 72 | - workflow:write 73 | allowedHosts: 74 | - github.com 75 | description: 76 | type: string 77 | defaultBranch: 78 | default: master 79 | type: string 80 | repoVisibility: 81 | type: string 82 | enum: ['private', 'public', 'internal'] 83 | requireCodeOwnerReviews: 84 | type: boolean 85 | 86 | collaborators: 87 | type: array 88 | items: 89 | type: 'object' 90 | required: ['username', 'access'] 91 | properties: 92 | access: 93 | type: 'string' 94 | description: 'The type of access for the user' 95 | enum: ['push', 'pull', 'admin', 'maintain', 'triage'] 96 | username: 97 | type: 'string' 98 | description: 'The username or group' 99 | topics: 100 | type: array 101 | items: 102 | type: string 103 | 104 | steps: 105 | - id: createFile 106 | name: Create File 107 | action: roadiehq:utils:fs:write 108 | input: 109 | path: ${{ parameters.createPath }} 110 | content: ${{ parameters.createContent }} 111 | 112 | - id: sleep 113 | name: Sleep 114 | action: roadiehq:utils:sleep 115 | input: 116 | amount: ${{ parameters.amount }} 117 | 118 | - id: appendFile 119 | name: Append File 120 | action: roadiehq:utils:fs:append 121 | input: 122 | path: ${{ parameters.appendPath }} 123 | content: | 124 | 125 | ${{ parameters.appendContent }} 126 | 127 | - id: zip 128 | name: Zip 129 | action: roadiehq:utils:zip 130 | input: 131 | path: ${{ parameters.zipPath }} 132 | outputPath: ${{ parameters.zipOutputPath }} 133 | 134 | - id: publish 135 | name: Create repository 136 | action: publish:github 137 | input: 138 | repoUrl: ${{ parameters.repoUrl }} 139 | topics: ${{ parameters.topics }} 140 | collaborators: ${{ parameters.collaborators }} 141 | repoVisibility: ${{ parameters.repoVisibility }} 142 | defaultBranch: ${{ parameters.defaultBranch }} 143 | requireCodeOwnerReviews: ${{parameters.requireCodeOwnerReviews}} 144 | description: ${{ parameters.description }} 145 | token: ${{ secrets.USER_OAUTH_TOKEN }} 146 | 147 | output: 148 | path: ${{ steps.zip.output.path }} 149 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 The Backstage Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { defineConfig, devices } from '@playwright/test'; 18 | //import { generateProjects } from '@backstage/e2e-test-utils/playwright'; 19 | 20 | /** 21 | * See https://playwright.dev/docs/test-configuration. 22 | */ 23 | export default defineConfig({ 24 | /* Run tests in files in parallel */ 25 | fullyParallel: true, 26 | /* Opt out of parallel tests on CI. */ 27 | workers: process.env.CI ? 1 : undefined, 28 | timeout: 30_000, 29 | 30 | expect: { 31 | timeout: 5_000, 32 | }, 33 | 34 | // Run your local dev server before starting the tests 35 | webServer: [ 36 | { 37 | command: 'yarn start', 38 | port: 3000, 39 | reuseExistingServer: true, 40 | timeout: 60_000, 41 | }, 42 | { 43 | command: 'yarn start-backend', 44 | port: 7007, 45 | reuseExistingServer: true, 46 | timeout: 60_000, 47 | }, 48 | { 49 | command: 'http-server -p 8090 --cors 2>&1', 50 | port: 8090, 51 | reuseExistingServer: true, 52 | timeout: 60_000, 53 | }, 54 | ], 55 | 56 | forbidOnly: !!process.env.CI, 57 | 58 | retries: process.env.CI ? 2 : 0, 59 | 60 | reporter: [['html', { open: 'never', outputFolder: 'e2e-test-report' }]], 61 | 62 | use: { 63 | actionTimeout: 0, 64 | baseURL: 65 | //process.env.PLAYWRIGHT_URL ?? 66 | 'http://localhost:3000', 67 | screenshot: 'on', 68 | trace: 'on', 69 | video: 'on', 70 | headless: true, 71 | }, 72 | 73 | outputDir: 'node_modules/.cache/e2e-test-results', 74 | 75 | projects: //generateProjects(), // Find all packages with e2e-test folders 76 | [ 77 | { 78 | name: 'chromium', 79 | testDir: "packages/app/e2e-tests", 80 | 81 | use: { 82 | ...devices['Desktop Chrome'] 83 | }, 84 | } 85 | ] 86 | }); 87 | -------------------------------------------------------------------------------- /plugins/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | var path = require('path'); 17 | 18 | module.exports = { 19 | root: true, 20 | plugins: ['notice'], 21 | rules: { 22 | 'notice/notice': [ 23 | 'error', 24 | { 25 | // eslint-disable-next-line no-restricted-syntax 26 | templateFile: path.resolve(__dirname, '../scripts/copyright-header.txt'), 27 | templateVars: { 28 | NAME: 'Oriflame', 29 | }, 30 | varRegexps: { NAME: /(Oriflame)/ }, 31 | onNonMatchingHeader: 'replace', 32 | }, 33 | ], 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /plugins/score-card/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 2 | -------------------------------------------------------------------------------- /plugins/score-card/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @oriflame/backstage-plugin-score-card 2 | 3 | ## 0.9.1 4 | 5 | ### Patch Changes 6 | 7 | - c3cbaf0: Updated dependency `react-router` to `6.26.2`. 8 | Updated dependency `react-router-dom` to `6.26.2`. 9 | 10 | ## 0.9.0 11 | 12 | ### Minor Changes 13 | 14 | - 35c2578: Alternative way to dynamically set location of JSON file through the annotations field of catalog-info.yaml. To achieve this, configuration of scorecard/jsonDataUrl alongside github.com/project-slug annotations is required within the catalog-info.yaml file. 15 | - 500e616: Added config options to customize reviewer and review date display behavior 16 | 17 | ## 0.8.0 18 | 19 | ### Minor Changes 20 | 21 | - 32cc2fb: Upgraded to Backstage core 1.22.1, support for React v18, Node 20, Cypress e2e test migrated to [Playwright](https://playwright.dev/) and more. 22 | 23 | BREAKING CHANGES: 24 | 25 | - `EntityScoreExtended` new required property `id:string` added to fullfill the Material UI Table row ID needs. Fix your clients by providing e.g. `stringifyEntityRef(score.entityRef)` as a value for the id. 26 | 27 | ## 0.7.1 28 | 29 | ### Patch Changes 30 | 31 | - 7b37b37: Added a configuration option to fetch all entities from the catalog to avoid "414 Request-URI Too Large" error (fix #184) 32 | 33 | ## 0.7.0 34 | 35 | ### Minor Changes 36 | 37 | - bd30933: wikiLinkTemplate is now optionnal 38 | 39 | ## 0.6.3 40 | 41 | ### Patch Changes 42 | 43 | - e8c6214: Bumped backstage core libs from 1.8.3 to 1.10.1 44 | 45 | ## 0.6.2 46 | 47 | ### Patch Changes 48 | 49 | - bbcae3e: Propagate title, subtitle and ScoreCardTableProps to ScoreBoardPage 50 | 51 | ## 0.6.1 52 | 53 | ### Patch Changes 54 | 55 | - d434c7d: Add property to filter scores by entity kind. 56 | 57 | ## 0.6.0 58 | 59 | ### Minor Changes 60 | 61 | - ec3991d: Allow any entity to be scored. 62 | 63 | BREAKING CHANGES: 64 | 65 | - `SystemScore` is renamed to `EntityScore` (and in a similar fashion all other `System*` components) 66 | - `systemEntityName` in `EntityScore` is replaced by `entityRef` 67 | - URL path to json files is changed from `{jsonDataUrl}/{systemEntityName}.json` to `{jsonDataUrl}/{entity-namespace}/{entity-kind}/{entity-name}.json` 68 | 69 | ### Patch Changes 70 | 71 | - 398f28d: bump version for backstage core components to 1.8.3 72 | 73 | ## 0.5.7 74 | 75 | ### Patch Changes 76 | 77 | - 111a8c1: Fix ScoreCardTable export and update its documentation 78 | 79 | ## 0.5.6 80 | 81 | ### Patch Changes 82 | 83 | - 3a2f7b5: Expose ScoreCardTable for usage outside ScoreBoardPage 84 | 85 | ## 0.5.5 86 | 87 | ### Patch Changes 88 | 89 | - 12cb1f1: Improve extensibility by adding `scoreLabel`, to override `scorePercent` and adding a `title` prop to ScoreCardTable component 90 | 91 | ## 0.5.4 92 | 93 | ### Patch Changes 94 | 95 | - ac868b1: bumped to 1.7.0 backstage core version 96 | 97 | ## 0.5.3 98 | 99 | ### Patch Changes 100 | 101 | - 71b3b50: revert of React18 support 102 | 103 | ## 0.5.2 104 | 105 | ### Patch Changes 106 | 107 | - 4da6557: Updated dependency `react` to `^16.13.1 || ^17.0.0 || ^18.0.0`. 108 | Updated dependency `react-dom` to `^16.13.1 || ^17.0.0 || ^18.0.0`. 109 | 110 | ## 0.5.1 111 | 112 | ### Patch Changes 113 | 114 | - d697d52: Bumped to backstage-core 1.6.0 115 | 116 | ## 0.5.0 117 | 118 | ### Minor Changes 119 | 120 | - a94816d: New config [wikiLinkTemplate] 121 | 122 | ## 0.4.2 123 | 124 | ### Patch Changes 125 | 126 | - c9c49d1: Fixed repository url (package metadata) 127 | 128 | ## 0.4.1 129 | 130 | ### Patch Changes 131 | 132 | - bumped dependencies 133 | -------------------------------------------------------------------------------- /plugins/score-card/config.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export interface Config { 18 | /** 19 | * Extra configuration for score card plugin 20 | */ 21 | scorecards: { 22 | /** 23 | * The public absolute root URL with json file defining the score card entries. 24 | * @visibility frontend 25 | */ 26 | jsonDataUrl?: string; 27 | display?: { 28 | /** 29 | * Whether to display the kind column in the score card table. 30 | * @visibility frontend 31 | */ 32 | kind?: string; 33 | /** 34 | * Whether to display the owner column in the score card table. 35 | * @visibility frontend 36 | */ 37 | owner?: string; 38 | /** 39 | * Whether to display the reviewer column in the score card table. 40 | * @visibility frontend 41 | */ 42 | reviewer?: string; 43 | /** 44 | * Whether to display the review date column in the score card table. 45 | * @visibility frontend 46 | */ 47 | reviewDate?: string; 48 | }; 49 | /** 50 | * The template for the link to the wiki, e.g. "https://TBD/XXX/_wiki/wikis/XXX.wiki/{id}" 51 | * @visibility frontend 52 | */ 53 | wikiLinkTemplate?: string; 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /plugins/score-card/dev/app-config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | title: Score-card plugin 3 | baseUrl: http://localhost:3024 4 | 5 | backend: 6 | baseUrl: http://localhost:6024 7 | listen: 8 | port: 6024 #shall be replaced in env specific configuration 9 | database: 10 | client: sqlite3 #shall be replaced in env specific configuration 11 | connection: ':memory:' #shall be replaced in env specific configuration 12 | 13 | scorecards: 14 | jsonDataUrl: http://127.0.0.1:8090/sample-data/ #this needs to be served via http-server as we get http 200 with WDS server instead of http 404 for nonExistent entity 15 | display: 16 | reviewer: always 17 | reviewDate: always 18 | -------------------------------------------------------------------------------- /plugins/score-card/dev/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import React from 'react'; 17 | import { createDevApp, DevAppPageOptions } from '@backstage/dev-utils'; 18 | import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; 19 | import { Content, Header, HeaderLabel, Page } from '@backstage/core-components'; 20 | import { catalogApiRef, EntityProvider } from '@backstage/plugin-catalog-react'; 21 | 22 | import { 23 | scoreCardPlugin, 24 | ScoreBoardPage, 25 | EntityScoreCardContent, 26 | } from '../src/plugin'; 27 | 28 | import { 29 | entityAudioPlaybackSystem, 30 | entityTeamC, 31 | entityWithoutScoringData, 32 | entityGuestUser, 33 | } from './sample-entities'; 34 | import { CatalogEntityPage } from '@backstage/plugin-catalog'; 35 | import { 36 | CatalogRequestOptions, 37 | GetEntitiesRequest, 38 | GetEntitiesResponse, 39 | } from '@backstage/catalog-client'; 40 | 41 | const entityContentPage = ( 42 | entity: Entity, 43 | title: string, 44 | path: string, 45 | ): DevAppPageOptions => { 46 | return { 47 | element: ( 48 | 49 | 50 |
54 | 55 |
56 | 57 | {entity.kind === 'System' ? : <>} 58 | 59 |
60 |
61 | ), 62 | title, 63 | path, 64 | }; 65 | }; 66 | 67 | localStorage.setItem('sidebarPinState', 'true'); 68 | 69 | const mockEntities = [ 70 | entityAudioPlaybackSystem, 71 | entityTeamC, 72 | entityWithoutScoringData, 73 | entityGuestUser, 74 | ] as unknown as Entity[]; 75 | 76 | createDevApp() 77 | .registerPlugin(scoreCardPlugin) 78 | .registerApi({ 79 | api: catalogApiRef, 80 | deps: {}, 81 | factory: () => 82 | ({ 83 | async getEntities( 84 | _request?: GetEntitiesRequest, 85 | _options?: CatalogRequestOptions, 86 | ) { 87 | return new Promise((resolve, _reject) => { 88 | resolve({ 89 | items: mockEntities.slice(), 90 | }); 91 | }); 92 | }, 93 | async getEntityByName( 94 | compoundName: CompoundEntityRef, 95 | _options?: CatalogRequestOptions, 96 | ) { 97 | return new Promise((resolve, _reject) => { 98 | resolve( 99 | mockEntities.find(e => e.metadata.name === compoundName.name), 100 | ); 101 | }); 102 | }, 103 | async getEntityByRef( 104 | _entityRef: string | CompoundEntityRef, 105 | _options?: CatalogRequestOptions, 106 | ): Promise { 107 | return new Promise((resolve, _reject) => { 108 | resolve(undefined); // like this it won't throw an error when opening an entity.. we don't want to mock the whole catalog api though 109 | }); 110 | }, 111 | } as unknown as typeof catalogApiRef.T), 112 | }) 113 | .addPage( 114 | entityContentPage( 115 | entityAudioPlaybackSystem, 116 | 'audio-playback', 117 | 'score-card/audio-playback/score', 118 | ), 119 | ) 120 | .addPage( 121 | entityContentPage(entityWithoutScoringData, 'Not Found test', 'notFound'), 122 | ) 123 | .addPage({ 124 | path: '/catalog/:kind/:namespace/:name', 125 | element: , 126 | }) 127 | .addPage({ 128 | element: , 129 | title: 'Score Board', 130 | path: '/score-card', 131 | }) 132 | .render(); 133 | -------------------------------------------------------------------------------- /plugins/score-card/dev/sample-entities.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; 18 | 19 | export const entityAudioPlaybackSystem: Entity = { 20 | apiVersion: 'backstage.io/v1alpha1', 21 | kind: 'System', 22 | metadata: { 23 | name: 'audio-playback', 24 | description: 'Audio playback system', 25 | annotations: {}, 26 | }, 27 | relations: [ 28 | { 29 | type: RELATION_OWNED_BY, 30 | targetRef: 'Group:team-c', 31 | }, 32 | ], 33 | }; 34 | 35 | export const entityWithoutScoringData: Entity = { 36 | apiVersion: 'backstage.io/v1alpha1', 37 | kind: 'System', 38 | metadata: { 39 | name: 'no-scoring-data-system', 40 | description: 'System without scoring data', 41 | annotations: {}, 42 | }, 43 | }; 44 | 45 | export const entityTeamC: Entity = { 46 | apiVersion: 'backstage.io/v1alpha1', 47 | kind: 'Group', 48 | metadata: { 49 | name: 'team-c', 50 | description: 'Team C', 51 | annotations: {}, 52 | }, 53 | }; 54 | 55 | export const entityGuestUser: Entity = { 56 | apiVersion: 'backstage.io/v1alpha1', 57 | kind: 'User', 58 | metadata: { 59 | name: 'Guest', 60 | description: 'Guest user', 61 | annotations: {}, 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /plugins/score-card/docs/.assets/score-board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oriflame/backstage-plugins/143e210c8c87fca3c9f039d75757141b7d9e264d/plugins/score-card/docs/.assets/score-board.png -------------------------------------------------------------------------------- /plugins/score-card/docs/.assets/score-card-detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oriflame/backstage-plugins/143e210c8c87fca3c9f039d75757141b7d9e264d/plugins/score-card/docs/.assets/score-card-detail.png -------------------------------------------------------------------------------- /plugins/score-card/docs/.assets/score-card-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oriflame/backstage-plugins/143e210c8c87fca3c9f039d75757141b7d9e264d/plugins/score-card/docs/.assets/score-card-table.png -------------------------------------------------------------------------------- /plugins/score-card/docs/.assets/score-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oriflame/backstage-plugins/143e210c8c87fca3c9f039d75757141b7d9e264d/plugins/score-card/docs/.assets/score-card.png -------------------------------------------------------------------------------- /plugins/score-card/docs/.assets/sharepoint-form-editation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oriflame/backstage-plugins/143e210c8c87fca3c9f039d75757141b7d9e264d/plugins/score-card/docs/.assets/sharepoint-form-editation.png -------------------------------------------------------------------------------- /plugins/score-card/docs/.assets/system-scoring.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oriflame/backstage-plugins/143e210c8c87fca3c9f039d75757141b7d9e264d/plugins/score-card/docs/.assets/system-scoring.pdf -------------------------------------------------------------------------------- /plugins/score-card/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@oriflame/backstage-plugin-score-card", 3 | "version": "0.9.1", 4 | "main": "src/index.ts", 5 | "types": "src/index.ts", 6 | "license": "Apache-2.0", 7 | "publishConfig": { 8 | "access": "public", 9 | "main": "dist/index.esm.js", 10 | "types": "dist/index.d.ts" 11 | }, 12 | "backstage": { 13 | "role": "frontend-plugin" 14 | }, 15 | "homepage": "https://github.com/Oriflame/backstage-plugins/tree/main/plugins/score-card", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/Oriflame/backstage-plugins", 19 | "directory": "plugins/score-card" 20 | }, 21 | "keywords": [ 22 | "backstage", 23 | "scoring" 24 | ], 25 | "scripts": { 26 | "start": "backstage-cli package start", 27 | "start:dev": "http-server -p 8090 --cors & yarn start --config dev/app-config.yaml", 28 | "build": "backstage-cli package build", 29 | "lint": "backstage-cli package lint", 30 | "test": "backstage-cli package test", 31 | "clean": "backstage-cli package clean", 32 | "prepack": "backstage-cli package prepack", 33 | "postpack": "backstage-cli package postpack" 34 | }, 35 | "dependencies": { 36 | "@backstage/catalog-model": "^1.4.3", 37 | "@backstage/config": "^1.1.1", 38 | "@backstage/core-components": "^0.13.10", 39 | "@backstage/core-plugin-api": "^1.8.2", 40 | "@backstage/integration": "^1.9.0", 41 | "@backstage/integration-react": "^1.1.24", 42 | "@backstage/plugin-catalog-common": "^1.0.20", 43 | "@backstage/plugin-catalog-react": "^1.9.3", 44 | "@backstage/theme": "^0.5.0", 45 | "@backstage/types": "^1.1.1", 46 | "@backstage/version-bridge": "^1.0.7", 47 | "@material-ui/core": "^4.12.2", 48 | "@material-ui/icons": "^4.11.2", 49 | "@material-ui/lab": "^4.0.0-alpha.57", 50 | "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", 51 | "react-use": "^17.2.4" 52 | }, 53 | "peerDependencies": { 54 | "react": "^16.13.1 || ^17.0.0 || ^18.0.0", 55 | "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0", 56 | "react-router-dom": "6.0.0-beta.0 || ^6.3.0" 57 | }, 58 | "devDependencies": { 59 | "@backstage/catalog-client": "^1.5.2", 60 | "@backstage/cli": "^0.25.1", 61 | "@backstage/core-app-api": "^1.11.3", 62 | "@backstage/dev-utils": "^1.0.26", 63 | "@backstage/plugin-catalog": "^1.16.1", 64 | "@backstage/test-utils": "^1.4.7", 65 | "@testing-library/dom": "^9.0.0", 66 | "@testing-library/jest-dom": "^6.0.0", 67 | "@testing-library/react": "^14.0.0", 68 | "@testing-library/user-event": "^14.0.0", 69 | "@types/jest": "*", 70 | "@types/node": "^20.11.5", 71 | "cross-fetch": "3.1.5", 72 | "http-server": "14.1.1", 73 | "msw": "1.3.2" 74 | }, 75 | "files": [ 76 | "dist", 77 | "config.d.ts" 78 | ], 79 | "configSchema": "config.d.ts" 80 | } 81 | -------------------------------------------------------------------------------- /plugins/score-card/sample-data/README.md: -------------------------------------------------------------------------------- 1 | # Sample data 2 | 3 | In this folder you may see a sample data for scoring: 4 | 5 | - `@template.json` contains a template for scoring. Description and possible values to build Sharepoint list. 6 | - `all.json` contains all data from all entities. Grouped and calculated sums, results. It's updated during integration pipeline. 7 | - `{namespace}/{kind}/{name}.json` example data for scoring of entities. 8 | -------------------------------------------------------------------------------- /plugins/score-card/src/api/ScoringDataApi.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Entity } from '@backstage/catalog-model'; 17 | import { createApiRef } from '@backstage/core-plugin-api'; 18 | import { EntityScoreExtended } from './types'; 19 | 20 | export const scoringDataApiRef = createApiRef({ 21 | id: 'plugin.scoringdata.service', 22 | }); 23 | 24 | export type ScoringDataApi = { 25 | getScore(entity?: Entity): Promise; 26 | getAllScores( 27 | entityKindFilter?: string[], 28 | entity?: Entity, 29 | ): Promise; 30 | }; 31 | -------------------------------------------------------------------------------- /plugins/score-card/src/api/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | export type { EntityScore, EntityScoreArea, EntityScoreEntry } from './types'; 17 | export { ScoreSuccessEnum } from './types'; 18 | export { scoringDataApiRef } from './ScoringDataApi'; 19 | export type { ScoringDataApi } from './ScoringDataApi'; 20 | export { ScoringDataJsonClient } from './ScoringDataJsonClient'; 21 | -------------------------------------------------------------------------------- /plugins/score-card/src/api/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { CompoundEntityRef } from '@backstage/catalog-model'; 17 | 18 | export enum ScoreSuccessEnum { 19 | Success = 'success', 20 | AlmostSuccess = 'almost-success', 21 | Partial = 'partial', 22 | AlmostFailure = 'almost-failure', 23 | Failure = 'failure', 24 | } 25 | 26 | export interface EntityScore { 27 | entityRef: CompoundEntityRef; 28 | generatedDateTimeUtc: Date | string; 29 | scorePercent: number; 30 | scoreLabel?: string; 31 | scoreSuccess: ScoreSuccessEnum; 32 | scoringReviewer: string | CompoundEntityRef | undefined | null; 33 | scoringReviewDate: Date | string | undefined | null; 34 | areaScores: EntityScoreArea[]; 35 | } 36 | 37 | export interface EntityScoreArea { 38 | id: number; 39 | title: string; 40 | scorePercent: number; 41 | scoreLabel?: string; 42 | scoreSuccess: ScoreSuccessEnum; 43 | scoreEntries: EntityScoreEntry[]; 44 | } 45 | 46 | export interface EntityScoreEntry { 47 | id: number; 48 | title: string; 49 | isOptional: boolean; 50 | scorePercent: number; 51 | scoreLabel?: string; 52 | scoreSuccess: ScoreSuccessEnum; 53 | scoreHints: string | string[]; 54 | details: string; 55 | } 56 | 57 | export interface EntityScoreExtended extends EntityScore { 58 | id: string; 59 | owner: CompoundEntityRef | undefined; 60 | reviewer: CompoundEntityRef | undefined; 61 | reviewDate: Date | undefined; 62 | } 63 | -------------------------------------------------------------------------------- /plugins/score-card/src/components/EntityScoreCardTable/EntityScoreCardTable.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import React from 'react'; 17 | import { ScoreCardTable } from '../ScoreCardTable/ScoreCardTable'; 18 | import { useEntity } from '@backstage/plugin-catalog-react'; 19 | 20 | type ScoreCardTableProps = { 21 | title?: string; 22 | entityKindFilter?: string[]; 23 | }; 24 | 25 | /** 26 | * EntityScoreCardTable is a wrapper around ScoreCardTable 27 | * that automatically fetches the entity from the catalog and 28 | * passes it to ScoreCardTable so that the entity can be used to 29 | * load information about the location of the scores 30 | **/ 31 | export const EntityScoreCardTable = ({ 32 | title, 33 | entityKindFilter, 34 | }: ScoreCardTableProps) => { 35 | const { entity } = useEntity(); 36 | 37 | return ( 38 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /plugins/score-card/src/components/EntityScoreCardTable/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ export { EntityScoreCardTable } from './EntityScoreCardTable'; 16 | -------------------------------------------------------------------------------- /plugins/score-card/src/components/ScoreBoardPage/ScoreBoardPage.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import React from 'react'; 17 | import { Grid } from '@material-ui/core'; 18 | import { 19 | Header, 20 | Page, 21 | Content, 22 | ContentHeader, 23 | HeaderLabel, 24 | SupportButton, 25 | } from '@backstage/core-components'; 26 | import { ScoreCardTable } from '../ScoreCardTable'; 27 | 28 | type ScoreBoardPageProps = { 29 | title?: string; 30 | subTitle?: string; 31 | tableTitle?: string; 32 | entityKindFilter?: string[]; 33 | }; 34 | 35 | export const ScoreBoardPage = ({ 36 | title, 37 | subTitle, 38 | tableTitle, 39 | entityKindFilter, 40 | }: ScoreBoardPageProps) => ( 41 | 42 |
46 | 47 | 48 |
49 | 50 | 51 | 52 | In this table you may see overview of entity scores. 53 | 54 | 55 | 56 | 57 | 61 | 62 | 63 | 64 |
65 | ); 66 | -------------------------------------------------------------------------------- /plugins/score-card/src/components/ScoreBoardPage/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | export { ScoreBoardPage } from './ScoreBoardPage'; 17 | -------------------------------------------------------------------------------- /plugins/score-card/src/components/ScoreCard/ScoreCard.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import React from 'react'; 17 | import { act, render } from '@testing-library/react'; 18 | import { ScoreCard } from './ScoreCard'; 19 | import { TestApiProvider } from '@backstage/test-utils'; 20 | import { ScoringDataApi, scoringDataApiRef } from '../../api'; 21 | import { Entity } from '@backstage/catalog-model'; 22 | import { EntityScoreExtended } from '../../api/types'; 23 | import { 24 | configApiRef, 25 | errorApiRef, 26 | githubAuthApiRef, 27 | } from '@backstage/core-plugin-api'; 28 | import { lightTheme } from '@backstage/theme'; 29 | import { ThemeProvider } from '@material-ui/core'; 30 | import { EntityProvider } from '@backstage/plugin-catalog-react'; 31 | import { ConfigReader } from '@backstage/core-app-api'; 32 | 33 | const sharedConfigApiMock = new ConfigReader({ 34 | scorecards: { wikiLinkTemplate: 'https://mocked-wiki-url/{id}/{title}' }, 35 | }); 36 | const sharedErrorApi = { post: () => {} }; 37 | 38 | const mockAuth = { 39 | getAccessToken: jest.fn(), 40 | }; 41 | 42 | const sharedGithubAuthApi = mockAuth; // Mocked GithubAuth API 43 | 44 | describe('ScoreCard-EmptyData', () => { 45 | class MockClient implements ScoringDataApi { 46 | getScore( 47 | _entity?: Entity | undefined, 48 | ): Promise { 49 | return new Promise( 50 | (resolve, _reject) => { 51 | resolve(undefined); 52 | }, 53 | ); 54 | } 55 | getAllScores(): Promise { 56 | throw new Error('Method not implemented.'); 57 | } 58 | } 59 | const mockClient = new MockClient(); 60 | 61 | const entity: Entity = { 62 | apiVersion: 'v1', 63 | kind: 'System', 64 | metadata: { 65 | name: 'audio-playback', 66 | }, 67 | }; 68 | 69 | it('should render a progress bar', async () => { 70 | jest.useFakeTimers(); 71 | 72 | const { getByTestId, findByTestId } = render( 73 | 74 | 82 | 83 | 84 | 85 | 86 | , 87 | ); 88 | 89 | act(() => { 90 | jest.advanceTimersByTime(250); 91 | }); 92 | expect(getByTestId('progress')).toBeInTheDocument(); 93 | 94 | await findByTestId('score-card-no-data'); 95 | jest.useRealTimers(); 96 | }); 97 | }); 98 | 99 | describe('ScoreCard-TestWithData', () => { 100 | class MockClient implements ScoringDataApi { 101 | getScore( 102 | _entity?: Entity | undefined, 103 | ): Promise { 104 | return new Promise( 105 | (resolve, _reject) => { 106 | const sampleData = require('../../../sample-data/default/system/podcast.json'); 107 | resolve(sampleData); 108 | }, 109 | ); 110 | } 111 | getAllScores(): Promise { 112 | throw new Error('Method not implemented.'); 113 | } 114 | } 115 | 116 | const mockClient = new MockClient(); 117 | 118 | const entity: Entity = { 119 | apiVersion: 'v1', 120 | kind: 'System', 121 | metadata: { 122 | name: 'audio-playback', 123 | }, 124 | }; 125 | 126 | it('should render a progress bar', async () => { 127 | jest.useFakeTimers(); 128 | 129 | const { getByTestId, findByTestId } = render( 130 | 131 | 139 | 140 | 141 | 142 | 143 | , 144 | ); 145 | 146 | act(() => { 147 | jest.advanceTimersByTime(250); 148 | }); 149 | expect(getByTestId('progress')).toBeInTheDocument(); 150 | 151 | await findByTestId('score-card'); 152 | jest.useRealTimers(); 153 | }); 154 | 155 | it('should render the score label when provided', async () => { 156 | const { findByText } = render( 157 | 158 | 166 | 167 | 168 | 169 | 170 | , 171 | ); 172 | 173 | await findByText('Total score: C'); 174 | 175 | const codeScore = await findByText('Code'); 176 | expect(codeScore.querySelector('div')).toHaveTextContent('A'); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /plugins/score-card/src/components/ScoreCard/ScoreCard.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Entity } from '@backstage/catalog-model'; 18 | import { useEntity } from '@backstage/plugin-catalog-react'; 19 | import { Chip, Grid } from '@material-ui/core'; 20 | import { makeStyles } from '@material-ui/core/styles'; 21 | import React, { useEffect } from 'react'; 22 | import useAsync from 'react-use/lib/useAsync'; 23 | 24 | import { 25 | EmptyState, 26 | InfoCard, 27 | InfoCardVariants, 28 | Progress, 29 | Table, 30 | TableColumn, 31 | } from '@backstage/core-components'; 32 | import { configApiRef, errorApiRef, useApi } from '@backstage/core-plugin-api'; 33 | import { scoreToColorConverter } from '../../helpers/scoreToColorConverter'; 34 | import { getWarningPanel } from '../../helpers/getWarningPanel'; 35 | import { 36 | getScoreTableEntries, 37 | EntityScoreTableEntry, 38 | } from './helpers/getScoreTableEntries'; 39 | import { areaColumn } from './columns/areaColumn'; 40 | import { detailsColumn } from './columns/detailsColumn'; 41 | import { scorePercentColumn } from './columns/scorePercentColumn'; 42 | import { titleColumn } from './columns/titleColumn'; 43 | import { getReviewerLink } from './sub-components/getReviewerLink'; 44 | import { scoringDataApiRef } from '../../api'; 45 | import { useDisplayConfig } from '../../config/DisplayConfig'; 46 | 47 | // lets prepare some styles 48 | const useStyles = makeStyles(theme => ({ 49 | badgeLabel: { 50 | color: theme.palette.common.white, 51 | }, 52 | header: { 53 | padding: theme.spacing(2, 2, 2, 2.5), 54 | }, 55 | action: { 56 | margin: 0, 57 | }, 58 | disabled: { 59 | backgroundColor: theme.palette.background.default, 60 | }, 61 | })); 62 | 63 | // data loader 64 | const useScoringDataLoader = () => { 65 | const errorApi = useApi(errorApiRef); 66 | const scoringDataApi = useApi(scoringDataApiRef); 67 | const config = useApi(configApiRef); 68 | const { entity } = useEntity(); 69 | 70 | const { error, value, loading } = useAsync( 71 | async () => scoringDataApi.getScore(entity), 72 | [scoringDataApi, entity], 73 | ); 74 | 75 | useEffect(() => { 76 | if (error) { 77 | errorApi.post(error); 78 | } 79 | }, [error, errorApi]); 80 | 81 | const wikiLinkTemplate = 82 | config.getOptionalString('scorecards.wikiLinkTemplate') ?? ''; 83 | 84 | return { loading, value, wikiLinkTemplate, error }; 85 | }; 86 | 87 | export const ScoreCard = ({ 88 | variant = 'gridItem', 89 | }: { 90 | entity?: Entity; 91 | variant?: InfoCardVariants; 92 | }) => { 93 | // let's load the entity data from url defined in config etc 94 | const { 95 | loading, 96 | error, 97 | value: data, 98 | wikiLinkTemplate, 99 | } = useScoringDataLoader(); 100 | 101 | const classes = useStyles(); 102 | 103 | // let's prepare the "chip" used in the up-right card corner 104 | let gateLabel = 'Not computed'; 105 | const gateStyle = { 106 | margin: 0, 107 | backgroundColor: scoreToColorConverter(data?.scoreSuccess), 108 | }; 109 | if (data?.scorePercent || data?.scorePercent === 0) { 110 | const label = data?.scoreLabel ?? `${data.scorePercent} %`; 111 | gateLabel = `Total score: ${label}`; 112 | } 113 | const qualityBadge = !loading && ; 114 | 115 | // let's define the main table columns 116 | const columns: TableColumn[] = [ 117 | areaColumn(data), 118 | titleColumn(wikiLinkTemplate), 119 | detailsColumn, 120 | scorePercentColumn, 121 | ]; 122 | 123 | const allEntries = getScoreTableEntries(data); 124 | 125 | const displayPolicies = useDisplayConfig().getDisplayPolicies(); 126 | 127 | return ( 128 | 139 | {loading && } 140 | 141 | {error && getWarningPanel(error)} 142 | 143 | {!loading && !data && ( 144 |
145 | 150 |
151 | )} 152 | 153 | {!loading && data && ( 154 |
155 | 164 | 165 | title="Score of each requirement" 166 | options={{ 167 | search: true, 168 | paging: false, 169 | grouping: true, 170 | padding: 'dense', 171 | }} 172 | columns={columns} 173 | data={allEntries} 174 | components={{ 175 | Groupbar: () => null, // we do not want to display possibility to change grouping 176 | }} 177 | /> 178 | 179 | {getReviewerLink(data, displayPolicies)} 180 | 181 |
182 | )} 183 |
184 | ); 185 | }; 186 | -------------------------------------------------------------------------------- /plugins/score-card/src/components/ScoreCard/columns/areaColumn.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { TableColumn } from '@backstage/core-components'; 18 | import { Chip, Link } from '@material-ui/core'; 19 | import { scoreToColorConverter } from '../../../helpers/scoreToColorConverter'; 20 | import { EntityScoreTableEntry } from '../helpers/getScoreTableEntries'; 21 | import React from 'react'; 22 | import { EntityScoreExtended } from '../../../api/types'; 23 | 24 | export function areaColumn( 25 | value: EntityScoreExtended | null | undefined, 26 | ): TableColumn { 27 | return { 28 | title: 'Area', 29 | field: 'area', 30 | grouping: true, 31 | groupTitle: 'Area', 32 | defaultGroupOrder: 0, 33 | width: '1px', 34 | render: (data, type) => { 35 | if (type === 'group') { 36 | // we need to find the area..it's based on title (see allEntries reduce below) as we can used this for ordering too 37 | const area = value?.areaScores.find(a => a.title === data.toString()); 38 | const areaGateStyle: React.CSSProperties = { 39 | marginTop: '0.5rem', 40 | marginRight: '1rem', 41 | backgroundColor: scoreToColorConverter(area?.scoreSuccess), 42 | float: 'right', 43 | minWidth: '4rem', 44 | }; 45 | const areaGateLabel = area?.scoreLabel ?? `${area?.scorePercent} %`; 46 | return ( 47 | 48 | <> 49 | {data} 50 | 51 | 52 | 53 | ); 54 | } 55 | return {data.area}; 56 | }, 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /plugins/score-card/src/components/ScoreCard/columns/detailsColumn.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { MarkdownContent, TableColumn } from '@backstage/core-components'; 18 | import React from 'react'; 19 | import { EntityScoreTableEntry } from '../helpers/getScoreTableEntries'; 20 | 21 | export const detailsColumn: TableColumn = { 22 | title: 'Details', 23 | field: 'details', 24 | grouping: false, 25 | render: entityScoreEntry => { 26 | const scoreHints = (entityScoreEntry.scoreHints as string[])?.join?.(', '); 27 | const hints = scoreHints ?? entityScoreEntry.scoreHints; 28 | return ( 29 |
30 | 31 | {hints ? hints: {hints} : null} 32 |
33 | ); 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /plugins/score-card/src/components/ScoreCard/columns/scorePercentColumn.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { TableColumn } from '@backstage/core-components'; 18 | import { Chip } from '@material-ui/core'; 19 | import React from 'react'; 20 | import { scoreToColorConverter } from '../../../helpers/scoreToColorConverter'; 21 | import { EntityScoreTableEntry } from '../helpers/getScoreTableEntries'; 22 | 23 | export const scorePercentColumn: TableColumn = { 24 | title:
Score
, 25 | field: 'scorePercent', 26 | align: 'right', 27 | grouping: false, 28 | width: '1%', 29 | render: entityScoreEntry => { 30 | const chipStyle: React.CSSProperties = { 31 | margin: 0, 32 | backgroundColor: scoreToColorConverter(entityScoreEntry?.scoreSuccess), 33 | minWidth: '4rem', 34 | }; 35 | const label = entityScoreEntry?.scoreLabel ?? `${entityScoreEntry.scorePercent} %`; 36 | return typeof entityScoreEntry.scorePercent !== 'undefined' ? ( 37 | 38 | ) : null; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /plugins/score-card/src/components/ScoreCard/columns/titleColumn.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { TableColumn } from '@backstage/core-components'; 18 | import { Link } from '@material-ui/core'; 19 | import React from 'react'; 20 | import { EntityScoreTableEntry } from '../helpers/getScoreTableEntries'; 21 | import { getWikiUrl } from '../helpers/getWikiUrl'; 22 | 23 | export function titleColumn( 24 | wikiLinkTemplate: string, 25 | ): TableColumn { 26 | return { 27 | title:
Requirement
, 28 | field: 'title', 29 | grouping: false, 30 | width: '1%', 31 | render: entityScoreEntry => { 32 | const wikiUrl = getWikiUrl(wikiLinkTemplate, entityScoreEntry); 33 | return ( 34 | {wikiUrl ? ( 35 | 40 | {entityScoreEntry.title} 41 | 42 | ) : ( 43 | <>{entityScoreEntry.title} 44 | )} 45 | {entityScoreEntry.isOptional ? ' (Optional)' : null} 46 | 47 | ) 48 | }, 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /plugins/score-card/src/components/ScoreCard/helpers/getScoreTableEntries.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { EntityScoreEntry } from '../../../api'; 18 | import { EntityScoreExtended } from '../../../api/types'; 19 | 20 | // this is an interface used for table entries. We need to enrich the original EntityScoreEntry with the "area" group, see bellow allEntries reduce 21 | export interface EntityScoreTableEntry extends EntityScoreEntry { 22 | area: string; 23 | } 24 | /* 25 | here comes the tricky part. We have json like this: 26 | { 27 | ... 28 | areaScores: [ 29 | { 30 | id: 1, 31 | title: "documentation", 32 | ... 33 | scoreEntries : [ 34 | { 35 | id: 222, 36 | title: "readme" 37 | }, 38 | { 39 | id: 333, 40 | title: "RFP" 41 | }, 42 | ... other score entries 43 | ] 44 | }, 45 | ... other areas with other score entries 46 | ] 47 | } 48 | and we want to have a flat array of score entries for table grouped by area, e.g. 49 | [ 50 | { 51 | id: 222, 52 | title: "readme", 53 | area: "documentation", 54 | ... 55 | }, 56 | { 57 | id: 333, 58 | title: "RFP", 59 | area: "documentation", 60 | ... 61 | }, 62 | ... 63 | ] 64 | so we want to basically go through all areaScores and get all its entries to one big array with a new 65 | property "area" pointing back to the area so we can later on get the area score... 66 | */ 67 | export function getScoreTableEntries( 68 | value: EntityScoreExtended | null | undefined, 69 | ): EntityScoreTableEntry[] { 70 | if (!value || value.areaScores.length <= 0) return []; 71 | return value.areaScores.reduce( 72 | (entries, area) => 73 | entries.concat( 74 | area.scoreEntries.map(entry => { 75 | return { 76 | area: area.title, 77 | ...entry, 78 | }; 79 | }), 80 | ), 81 | [], 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /plugins/score-card/src/components/ScoreCard/helpers/getWikiUrl.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { getWikiUrl } from './getWikiUrl'; 17 | import { ScoreSuccessEnum, EntityScoreEntry } from '../../../api/types'; 18 | 19 | describe('helper-getWikiUrl', () => { 20 | const entry: EntityScoreEntry = { 21 | id: 123, 22 | title: 'some-score-title', 23 | isOptional: false, 24 | scorePercent: 50, 25 | scoreSuccess: ScoreSuccessEnum.Partial, 26 | scoreHints: 'score hints', 27 | details: 'lorem ipsum details', 28 | }; 29 | 30 | it('should replace known params', () => { 31 | const url: string = getWikiUrl('http://someurl/{id}/{title}', entry); 32 | expect(url).toEqual('http://someurl/123/some-score-title'); 33 | }); 34 | 35 | it('should handle null', () => { 36 | const url: string = getWikiUrl('http://someurl/{id}/{title}', null); 37 | expect(url).toEqual('http://someurl//'); 38 | }); 39 | 40 | it('should handle undefined', () => { 41 | const url: string = getWikiUrl('http://someurl/{id}/{title}', undefined); 42 | expect(url).toEqual('http://someurl//'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /plugins/score-card/src/components/ScoreCard/helpers/getWikiUrl.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { EntityScoreEntry } from '../../../api/types'; 18 | 19 | export function getWikiUrl( 20 | wikiLinkTemplate: string, 21 | entry: EntityScoreEntry | null | undefined, 22 | ): string { 23 | if (!entry) return wikiLinkTemplate.replace(/\{[^\}]+\}/g, ''); 24 | return wikiLinkTemplate.replace(/\{[^\}]+\}/g, matched => { 25 | const keyName = matched.substring(1, matched.length - 1); 26 | const value = entry[keyName as keyof EntityScoreEntry]; 27 | return !value ? '' : value.toString(); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /plugins/score-card/src/components/ScoreCard/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export { ScoreCard } from './ScoreCard'; 18 | -------------------------------------------------------------------------------- /plugins/score-card/src/components/ScoreCard/sub-components/getReviewerLink.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { EntityRefLink } from '@backstage/plugin-catalog-react'; 17 | import React from 'react'; 18 | import { EntityScoreExtended } from '../../../api/types'; 19 | import { DisplayPolicies, DisplayPolicy } from '../../../config/types'; 20 | 21 | export function getReviewerLink( 22 | value: EntityScoreExtended, 23 | displayPolicies: DisplayPolicies, 24 | ) { 25 | const displayReviewer = displayPolicies.reviewer !== DisplayPolicy.Never; 26 | const displayReviewDate = displayPolicies.reviewDate !== DisplayPolicy.Never; 27 | 28 | if (!displayReviewer && !displayReviewDate) { 29 | return null; 30 | } 31 | 32 | return ( 33 |
34 | {value.reviewer ? ( 35 | <> 36 | Review done 37 | {displayReviewer && ' by '} 38 | {displayReviewer && ( 39 | 40 | {value.reviewer?.name} 41 | 42 | )} 43 | {displayReviewDate && 44 | ` at 45 | ${(value.reviewDate 46 | ? value.reviewDate.toLocaleDateString() 47 | : 'unknown')}`} 48 | 49 | ) : ( 50 | <>Not yet reviewed. 51 | )} 52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /plugins/score-card/src/components/ScoreCardTable/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | export { ScoreCardTable } from './ScoreCardTable'; 17 | -------------------------------------------------------------------------------- /plugins/score-card/src/config/DisplayConfig.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { MockConfigApi } from '@backstage/test-utils'; 18 | import { DisplayConfig } from './DisplayConfig'; 19 | import { DisplayPolicy } from './types'; 20 | 21 | describe('display config', () => { 22 | it.each([ 23 | [ 24 | { reviewer: 'always', reviewDate: 'always' }, 25 | { 26 | reviewer: DisplayPolicy.Always, 27 | reviewDate: DisplayPolicy.Always, 28 | owner: DisplayPolicy.Always, 29 | kind: DisplayPolicy.Always, 30 | }, 31 | ], 32 | [ 33 | { reviewer: 'never', reviewDate: 'never' }, 34 | { 35 | reviewer: DisplayPolicy.Never, 36 | reviewDate: DisplayPolicy.Never, 37 | owner: DisplayPolicy.Always, 38 | kind: DisplayPolicy.Always, 39 | }, 40 | ], 41 | [ 42 | { reviewer: 'if-data-present', reviewDate: 'if-data-present' }, 43 | { 44 | reviewer: DisplayPolicy.IfDataPresent, 45 | reviewDate: DisplayPolicy.IfDataPresent, 46 | owner: DisplayPolicy.Always, 47 | kind: DisplayPolicy.Always, 48 | }, 49 | ], 50 | [ 51 | { reviewDate: 'if-data-present' }, 52 | { 53 | reviewer: DisplayPolicy.Always, 54 | reviewDate: DisplayPolicy.IfDataPresent, 55 | owner: DisplayPolicy.Always, 56 | kind: DisplayPolicy.Always, 57 | }, 58 | ], 59 | [ 60 | { reviewer: 'never' }, 61 | { 62 | reviewer: DisplayPolicy.Never, 63 | reviewDate: DisplayPolicy.Always, 64 | owner: DisplayPolicy.Always, 65 | kind: DisplayPolicy.Always, 66 | }, 67 | ], 68 | [ 69 | { owner: 'never' }, 70 | { 71 | reviewer: DisplayPolicy.Always, 72 | reviewDate: DisplayPolicy.Always, 73 | owner: DisplayPolicy.Never, 74 | kind: DisplayPolicy.Always, 75 | }, 76 | ], 77 | [ 78 | { owner: 'if-data-present' }, 79 | { 80 | reviewer: DisplayPolicy.Always, 81 | reviewDate: DisplayPolicy.Always, 82 | owner: DisplayPolicy.IfDataPresent, 83 | kind: DisplayPolicy.Always, 84 | }, 85 | ], 86 | [ 87 | { kind: 'never' }, 88 | { 89 | reviewer: DisplayPolicy.Always, 90 | reviewDate: DisplayPolicy.Always, 91 | owner: DisplayPolicy.Always, 92 | kind: DisplayPolicy.Never, 93 | }, 94 | ], 95 | [ 96 | {}, 97 | { 98 | reviewer: DisplayPolicy.Always, 99 | reviewDate: DisplayPolicy.Always, 100 | owner: DisplayPolicy.Always, 101 | kind: DisplayPolicy.Always, 102 | } 103 | ], 104 | ])('gets expected display policies from config', (policies, expected) => { 105 | const mockConfig = new MockConfigApi({ scorecards: { display: policies } }); 106 | 107 | const config = new DisplayConfig({ configApi: mockConfig }); 108 | 109 | const displayPolicies = config.getDisplayPolicies(); 110 | expect(displayPolicies).toEqual(expected); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /plugins/score-card/src/config/DisplayConfig.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { ConfigApi, configApiRef, useApi } from '@backstage/core-plugin-api'; 18 | import { DisplayPolicies, DisplayPolicy } from './types'; 19 | 20 | export class DisplayConfig { 21 | configApi: ConfigApi; 22 | 23 | constructor({ configApi }: { configApi: ConfigApi }) { 24 | this.configApi = configApi; 25 | } 26 | 27 | public getDisplayPolicies(): DisplayPolicies { 28 | const displayConfig = 29 | this.configApi.getOptionalConfig('scorecards.display'); 30 | return { 31 | reviewer: 32 | (displayConfig?.getOptionalString('reviewer') as DisplayPolicy) ?? 33 | DisplayPolicy.Always, 34 | owner: 35 | (displayConfig?.getOptionalString('owner') as DisplayPolicy) ?? 36 | DisplayPolicy.Always, 37 | kind: 38 | (displayConfig?.getOptionalString('kind') as DisplayPolicy) ?? 39 | DisplayPolicy.Always, 40 | reviewDate: 41 | (displayConfig?.getOptionalString('reviewDate') as DisplayPolicy) ?? 42 | DisplayPolicy.Always, 43 | }; 44 | } 45 | } 46 | 47 | export const useDisplayConfig = () => { 48 | const configApi = useApi(configApiRef); 49 | return new DisplayConfig({ configApi }); 50 | }; 51 | -------------------------------------------------------------------------------- /plugins/score-card/src/config/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export enum DisplayPolicy { 18 | IfDataPresent = 'if-data-present', 19 | Never = 'never', 20 | Always = 'always', 21 | } 22 | 23 | export interface DisplayPolicies { 24 | kind: DisplayPolicy, 25 | owner: DisplayPolicy, 26 | reviewer: DisplayPolicy; 27 | reviewDate: DisplayPolicy; 28 | } 29 | -------------------------------------------------------------------------------- /plugins/score-card/src/helpers/getWarningPanel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { ResponseErrorPanel } from '@backstage/core-components'; 17 | import React from 'react'; 18 | 19 | export const getWarningPanel = (error: Error) => { 20 | return ( 21 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /plugins/score-card/src/helpers/scoreToColorConverter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { createTheme } from '@material-ui/core/styles'; 17 | import { ScoreSuccessEnum } from '../api'; 18 | 19 | export const scoreToColorConverter = ( 20 | scoreSuccess: ScoreSuccessEnum | undefined, 21 | ): string => { 22 | const theme = createTheme(); 23 | if (typeof scoreSuccess === 'undefined') { 24 | return theme.palette.grey[500]; 25 | } 26 | switch (scoreSuccess) { 27 | // see palette https://coolors.co/72af50-acbf8c-e2e8b3-ffc055-eb6f35 28 | case ScoreSuccessEnum.Success: 29 | return 'rgb(114, 175, 80)'; 30 | case ScoreSuccessEnum.AlmostSuccess: 31 | return 'rgb(172, 191, 140)'; 32 | case ScoreSuccessEnum.Partial: 33 | return 'rgb(226, 232, 179)'; 34 | case ScoreSuccessEnum.AlmostFailure: 35 | return 'rgb(255, 192, 85)'; 36 | case ScoreSuccessEnum.Failure: 37 | return 'rgb(235, 111, 53)'; 38 | default: 39 | return theme.palette.grey[500]; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /plugins/score-card/src/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { 17 | scoreCardPlugin, 18 | ScoreCardTable, 19 | ScoreBoardPage, 20 | EntityScoreCardContent, 21 | } from '.'; 22 | 23 | describe('score-card exports', () => { 24 | it('should export plugin', () => { 25 | expect(scoreCardPlugin).toBeDefined(); 26 | }); 27 | 28 | it('should export ScoreCardTable', () => { 29 | expect(ScoreCardTable).toBeDefined(); 30 | }); 31 | it('should export ScoreBoardPage', () => { 32 | expect(ScoreBoardPage).toBeDefined(); 33 | }); 34 | it('should export EntityScoreCardContent', () => { 35 | expect(EntityScoreCardContent).toBeDefined(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /plugins/score-card/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | export { 17 | scoreCardPlugin, 18 | ScoreBoardPage, 19 | ScoreCardTable, 20 | EntityScoreCardContent, 21 | EntityScoreCardTable, 22 | } from './plugin'; 23 | 24 | /** 25 | * The TypeScript API for configuring System Scoring. 26 | */ 27 | export * from './api'; 28 | -------------------------------------------------------------------------------- /plugins/score-card/src/plugin.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { scoreCardPlugin, ScoreCardTable } from './plugin'; 17 | 18 | describe('score-card', () => { 19 | it('should export plugin', () => { 20 | expect(scoreCardPlugin).toBeDefined(); 21 | }); 22 | 23 | it('should export ScoreCardTable', () => { 24 | expect(ScoreCardTable).toBeDefined(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /plugins/score-card/src/plugin.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { 17 | createPlugin, 18 | createRoutableExtension, 19 | createComponentExtension, 20 | createApiFactory, 21 | fetchApiRef, 22 | configApiRef, 23 | } from '@backstage/core-plugin-api'; 24 | import { 25 | scmAuthApiRef, 26 | scmIntegrationsApiRef, 27 | } from '@backstage/integration-react'; 28 | import { catalogApiRef } from '@backstage/plugin-catalog-react'; 29 | import { ScoringDataJsonClient, scoringDataApiRef } from './api'; 30 | import { rootRouteRef } from './routes'; 31 | 32 | export { ScoreCardTable } from './components/ScoreCardTable'; 33 | 34 | export const scoreCardPlugin = createPlugin({ 35 | id: 'score-card', 36 | routes: { 37 | root: rootRouteRef, 38 | }, 39 | apis: [ 40 | createApiFactory({ 41 | api: scoringDataApiRef, 42 | deps: { 43 | configApi: configApiRef, 44 | catalogApi: catalogApiRef, 45 | fetchApi: fetchApiRef, 46 | scmAuthApi: scmAuthApiRef, 47 | scmIntegrationsApi: scmIntegrationsApiRef, 48 | }, 49 | factory: ({ 50 | configApi, 51 | catalogApi, 52 | fetchApi, 53 | scmAuthApi, 54 | scmIntegrationsApi, 55 | }) => { 56 | return new ScoringDataJsonClient({ 57 | configApi, 58 | catalogApi, 59 | fetchApi, 60 | scmAuthApi, 61 | scmIntegrationsApi, 62 | }); 63 | }, 64 | }), 65 | ], 66 | }); 67 | 68 | export const ScoreBoardPage = scoreCardPlugin.provide( 69 | createRoutableExtension({ 70 | name: 'score-board-page', 71 | component: () => 72 | import('./components/ScoreBoardPage').then(m => m.ScoreBoardPage), 73 | mountPoint: rootRouteRef, 74 | }), 75 | ); 76 | 77 | export const EntityScoreCardContent = scoreCardPlugin.provide( 78 | createComponentExtension({ 79 | name: 'score-board-card', 80 | component: { 81 | lazy: () => import('./components/ScoreCard').then(m => m.ScoreCard), 82 | }, 83 | }), 84 | ); 85 | 86 | export const EntityScoreCardTable = scoreCardPlugin.provide( 87 | createComponentExtension({ 88 | name: 'entity-score-card-table', 89 | component: { 90 | lazy: () => 91 | import('./components/EntityScoreCardTable').then( 92 | m => m.EntityScoreCardTable, 93 | ), 94 | }, 95 | }), 96 | ); 97 | -------------------------------------------------------------------------------- /plugins/score-card/src/routes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { createRouteRef } from '@backstage/core-plugin-api'; 17 | 18 | export const rootRouteRef = createRouteRef({ 19 | id: 'score-card', 20 | }); 21 | -------------------------------------------------------------------------------- /plugins/score-card/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Oriflame 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import '@testing-library/jest-dom'; 17 | import 'cross-fetch/polyfill'; 18 | -------------------------------------------------------------------------------- /plugins/score-card/tools/azure-devops-pipelines/README.md: -------------------------------------------------------------------------------- 1 | # System Scores and Sharepoint integration 2 | 3 | !TODO: update to generate generic entities and not ( uncompatible!) "system" data. 4 | 5 | What does it do: 6 | 7 | 1. Gets fresh item data from the list 8 | 2. Updates local `system-scores/xxx.json` and computes the score 9 | 3. Merges all scores to `system-scores/all.json` 10 | 4. uploads the score to azure storage account (if runs during PR it uploads to DEV, from master to LIVE) 11 | 5. pushes the changes to the git 12 | 13 | To test it locally go to `./tools/azure-devops-pipelines/scripts` and run `./Sync-Sharepoint.ps1 -action download -systemScoreFolder '../../sample-data' -listId '12345678-1234-aaaa-bbbb-1234567890aa' -siteURL 'https://yourOffice365Account.sharepoint.com/teams/some-site' -interactive`. 14 | 15 | ## Prerequisites and configuration: 16 | 17 | You need to configure Azure ARM connection to create a service principal in Azure DevOps project "Service connection" settings. It will be created with "contributor" role for either subscription or resource group. Since it does not need such permissions please **remove those permissions**. 18 | 19 | You need to add rights for the service principal in the sharepoint like described [here](https://docs.microsoft.com/en-us/sharepoint/dev/solution-guidance/security-apponly-azureacs). That is, you need to open and enter application id, lookup for it and grant following rights: 20 | 21 | ```xml 22 | 23 | 24 | 25 | ``` 26 | 27 | You may also need to assign permissions for the **sharepoint list** to the service principal: 28 | 29 | ```powershell 30 | # see also https://www.sharepointdiary.com/2016/05/sharepoint-online-grant-permission-to-list-library-using-powershell.html 31 | Install-Module -Name PnP.PowerShell -RequiredVersion 1.9.0 -Repository PSGallery -Scope CurrentUser -Force; 32 | $listId = '12345678-1234-aaaa-bbbb-1234567890aa'; # list ID 33 | $appId = '12345678-1234-aaaa-bbbb-1234567890bb'; # application id, see above 34 | $appDisplayName = 'BackstageCatalogSharepointIntegration'; # could be anything, but please keep it aligned with the name of the app 35 | $siteURL = "https://yourOffice365Account.sharepoint.com/teams/some-site"; 36 | Connect-PnPOnline -Url $SiteURL -DeviceLogin; 37 | Grant-PnPAzureADAppSitePermission -AppId $appId -DisplayName $appDisplayName -Site $siteURL -Permissions Read 38 | ``` 39 | 40 | You need to assign permissions for the **Azure Storage account**: 41 | 42 | Go to the [Azure Portal](https://portal.azure.com/), open desired storage accounts, go to the AIM blade and assign "Storage Blob Data Contributor" role for the above mentioned service principal. 43 | 44 | Also consider assigning **a condition** to allow editing only system-scores: 45 | 46 | ```text 47 | ( 48 | ( 49 | !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/delete'}) 50 | AND 51 | !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read'}) 52 | AND 53 | !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read'} AND SubOperationMatches{'Blob.Read.WithTagConditions'}) 54 | AND 55 | !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write'}) 56 | AND 57 | !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write'} AND SubOperationMatches{'Blob.Write.WithTagHeaders'}) 58 | AND 59 | !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/add/action'}) 60 | AND 61 | !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/add/action'} AND SubOperationMatches{'Blob.Write.WithTagHeaders'}) 62 | AND 63 | !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/move/action'}) 64 | ) 65 | OR 66 | ( 67 | @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringEquals 'name-of-the-folder' 68 | ) 69 | ) 70 | ``` 71 | 72 | ## How to create the Sharepoint list 73 | 74 | To prepare all fields in the Sharepoint list use [Update-SharepointSelfAssessmentList.ps1](scripts/Update-SharepointSelfAssessmentList.ps1) script. 75 | 76 | It will use [@template.json](../../sample-data/@template.json) to generate fields and possible values. 77 | -------------------------------------------------------------------------------- /plugins/score-card/tools/azure-devops-pipelines/integration-with-sharepoint.yaml: -------------------------------------------------------------------------------- 1 | # Updating system-scores from sharepoint and uploading to azure storage 2 | # should run periodically 3 | 4 | parameters: 5 | - name: agentPoolName 6 | displayName: 'Agent pool name' 7 | type: string 8 | default: 'Azure Pipelines' 9 | values: 10 | - Azure Pipelines # public cloud agents 11 | - 'TODO: add your own agent pool' # or delete this if you don't have any 12 | 13 | trigger: none 14 | 15 | # scheduled run: https://docs.microsoft.com/en-us/azure/devops/pipelines/process/scheduled-triggers?view=azure-devops&tabs=yaml 16 | schedules: 17 | - cron: "45 0,6,12,18 * * *" 18 | displayName: 'Every 6 hours at 45th minute' # example schedule 19 | branches: 20 | include: 21 | - main # TODO: change to your branch 22 | always: true 23 | 24 | jobs: 25 | - job: 26 | displayName: 'Integration job' 27 | pool: 28 | name: ${{ parameters.agentPoolName }} 29 | variables: 30 | - name: sourcePathForScoringData 31 | value: ./scoring-data # TODO: create folder with scoring data for your system/entities, see sample-data folder 32 | - name: sharepointListId 33 | value: '12345678-1234-aaaa-bbbb-1234567890aa' # TODO: guid for your list containing answers 34 | - name: sharepointSiteURL 35 | value: 'https://yourOffice365Account.sharepoint.com/teams/some-site' # TODO: url of the Office365 site you want to itegrate against 36 | steps: 37 | - template: ./templates/checkout-branch-properly.yaml 38 | 39 | # get items from sharpeoint list and updates local files 40 | - task: AzureCLI@2.208.0 41 | displayName: 'Update system-scores from sharepoint' 42 | inputs: 43 | azureSubscription: BackstageCatalogSharepointIntegration # TODO: create the service connection, see documentation 44 | scriptType: pscore 45 | scriptLocation: scriptPath 46 | scriptPath: './scripts/Sync-Sharepoint.ps1' 47 | addSpnToEnvironment: true 48 | arguments: 49 | -action 50 | download 51 | -systemScoreFolder 52 | '$(sourcePathForScoringData)' 53 | -listId 54 | '$(sharepointListId)' 55 | -siteURL 56 | '$(sharepointSiteURL)' 57 | pwsh: true 58 | 59 | # always (e.g. during PR) upload also to DEV storage. if this would cause any issue (like overwriting data from regular main run over branch data), remove it 60 | - task: AzureCLI@2.208.0 61 | displayName: 'Uploading to DEV storage' 62 | inputs: 63 | azureSubscription: BackstageCatalogSharepointIntegration 64 | scriptLocation: inlineScript 65 | inlineScript: | 66 | az storage blob upload-batch \ 67 | --destination 'system-scores' \ 68 | --account-name 'azureblobstorageaccount' \ 69 | --source '$(sourcePathForScoringData)' \ 70 | --pattern *.json \ 71 | --auth-mode login \ 72 | --overwrite 73 | # ^^^^ TODO fix account name, destination paths,... 74 | 75 | # when on MAIN branch upload to oribackstagedevpublic storage 76 | - task: AzureCLI@2.208.0 77 | displayName: 'Uploading to LIVE storage' 78 | inputs: 79 | azureSubscription: BackstageCatalogSharepointIntegration 80 | scriptLocation: inlineScript 81 | inlineScript: | 82 | az storage blob upload-batch \ 83 | --destination 'system-scores' \ 84 | --account-name 'azureblobstorageaccount' \ 85 | --source '$(sourcePathForScoringData)' \ 86 | --pattern *.json \ 87 | --auth-mode login \ 88 | --overwrite 89 | # ^^^^ TODO fix account name, destination paths,... 90 | condition: eq(variables['Build.SourceBranch'],'refs/heads/main') # TODO: fix branch name 91 | 92 | #commit&push changes 93 | - task: PowerShell@2.212.0 94 | displayName: 'Auto commit changes' 95 | inputs: 96 | filePath: './scripts/push-changes.ps1' 97 | arguments: 98 | -autogeneratedDefinitionsFolder 99 | '$(sourcePathForScoringData)' 100 | -outputFile 101 | '$(sourcePathForScoringData)/all.json' 102 | failOnStderr: true 103 | showWarnings: true 104 | pwsh: true 105 | workingDirectory: '$(Build.SourcesDirectory)/pipelines' 106 | -------------------------------------------------------------------------------- /plugins/score-card/tools/azure-devops-pipelines/scripts/functions/Connect-Sharepoint.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Connects to Sharepoint on $siteURL. Use with -interactive on local workstation 4 | 5 | .PARAMETER siteURL 6 | Site url, e.g. https://yourOffice365Account.sharepoint.com/teams/some-site 7 | 8 | .PARAMETER interactive 9 | Shall we ask for login? Usefull for local debugging. 10 | 11 | #> 12 | function Connect-Sharepoint ( 13 | [Parameter(Mandatory = $true)] 14 | [string] 15 | $siteURL, 16 | 17 | [bool] 18 | $interactive 19 | ) { 20 | Write-Host "Connecting to sharepoint..." -ForegroundColor DarkGray; 21 | if ($null -eq (Get-Command -Name 'Connect-PnPOnline' -ErrorAction SilentlyContinue)) { 22 | throw "Please install prerequisities: [Install-Module -Name PnP.PowerShell -RequiredVersion 1.9.0]. ## prerequisity, see also https://pnp.github.io/powershell/cmdlets/Connect-PnPOnline.html#interactive-login-for-multi-factor-authentication"; 23 | } 24 | 25 | if ($interactive) { 26 | try { 27 | Write-Host "Connecting to $siteURL (interactivelly)..." -ForegroundColor DarkGray; 28 | Connect-PnPOnline -CurrentCredentials -Url $SiteURL | Out-Null; 29 | } 30 | catch { 31 | Write-Host "Connecting to $siteURL (with device login)..." -ForegroundColor DarkGray; 32 | Connect-PnPOnline -Url $SiteURL -DeviceLogin #interactive did not worked 33 | } 34 | } 35 | else { 36 | Write-Host "Connecting to $siteURL (with service principal $env:servicePrincipalId)..." -ForegroundColor DarkGray; 37 | Connect-PnPOnline -Url $siteURL -ClientId $env:servicePrincipalId -ClientSecret $env:servicePrincipalKey -WarningAction Ignore; 38 | # connecting via AccessToken is not supported for SPO, see https://github.com/pnp/PnP-PowerShell/pull/2657 39 | # Write-Host "Obtaining oAuth token..." -ForegroundColor DarkGray; 40 | # $oAuthToken = Get-AzureToken; 41 | # Write-Host "Connecting to $siteURL via ($oAuthToken)..." -ForegroundColor DarkGray; 42 | # Connect-PnPOnline -Url $siteURL -AccessToken $oAuthToken; 43 | } 44 | Write-Host "Connected!" -ForegroundColor DarkGray; 45 | } -------------------------------------------------------------------------------- /plugins/score-card/tools/azure-devops-pipelines/scripts/functions/Get-Success.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Calculates the success based on $scorePercent 4 | #> 5 | function Get-Success($scorePercent) { 6 | switch ($scorePercent) { 7 | { $null -ne $_ -and $_ -ge 80 } { return "success" } 8 | { $null -ne $_ -and $_ -ge 60 -and $_ -lt 80 } { return "almost-success" } 9 | { $null -ne $_ -and $_ -ge 40 -and $_ -lt 60 } { return "partial" } 10 | { $null -ne $_ -and $_ -ge 20 -and $_ -lt 40 } { return "almost-failure" } 11 | { $null -ne $_ -and $_ -lt 20 } { return "failure" } 12 | Default { return "unknown" } 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /plugins/score-card/tools/azure-devops-pipelines/scripts/functions/Read-FromSharepoint.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Reads data from sharepoint and updates the item in hashtable 4 | #> 5 | function Read-FromSharepoint { 6 | [CmdLetBinding()] 7 | param ( 8 | $hashTable, 9 | $itemName, 10 | $spItem, 11 | $spFieldMatch, 12 | $convertByTemplate, 13 | $sharepointAction 14 | ) 15 | if ($null -eq $spFieldMatch) { 16 | $spFieldMatch = $itemName; 17 | } 18 | $spField = $spFields | Where-Object -Property Title -Match -Value $spFieldMatch; 19 | if ($null -ne $spField) { 20 | $spValue = $spItem.FieldValues[$spField.InternalName]; 21 | 22 | if ($null -ne $spValue -and $null -ne $convertByTemplate.scoreChoices) { 23 | $spValue = ($spValue -split ':')[0]; #value in SP is "choice: XXX%" 24 | $choiceObj = $convertByTemplate.scoreChoices | Where-Object { ($_.PSObject.Properties | Select-Object -ExpandProperty Name -First 1) -eq $spValue }; 25 | $spValue = $choiceObj."$spValue"; #we want to get percent value from template (percent can change over time) 26 | } 27 | 28 | if ($null -ne $convertByTemplate.scoreChecks) { 29 | $choices = $spValue; # already in a form of array 30 | $spValue = 0; #in case ($null -ne $spValue) we will have 0 as a result = failure 31 | foreach ($choice in $choices) { 32 | #each choice adds certain percent 33 | $choiceObj = $convertByTemplate.scoreChecks | Where-Object { ($_.PSObject.Properties | Select-Object -ExpandProperty Name -First 1) -eq $choice }; 34 | if ($null -ne $choiceObj) { 35 | $spValue += $choiceObj."$choice"; #we want to get percent value from template (percent can change over time) 36 | } 37 | } 38 | } 39 | 40 | if ($null -ne $spValue -and $spValue.GetType() -eq [double]) { 41 | $spValue = [int](100 * $spValue); #since we receive .xx from SP we need to have XX in json... 42 | } 43 | $hashTable[$itemName] = $spValue; 44 | } 45 | } -------------------------------------------------------------------------------- /plugins/score-card/tools/azure-devops-pipelines/scripts/push-changes.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Pushes changed to git repo if any 4 | 5 | #> 6 | [CmdLetBinding()] 7 | param( 8 | [Parameter(Mandatory = $true)] 9 | [string] 10 | $autogeneratedDefinitionsFolder, 11 | 12 | [Parameter(Mandatory = $true)] 13 | [string] 14 | $outputFile 15 | ) 16 | $ErrorActionPreference = 'Stop'; 17 | 18 | Write-host "Commit changes and pushing to remote..." -ForegroundColor Gray; 19 | 20 | $currentUserEmail = & git config user.email; 21 | if ($null -eq $currentUserEmail) { 22 | Write-host "Git user.email empty, setting up a 'service' one..." -ForegroundColor DarkGray; 23 | & git config user.email "pipeline@oriflame.com"; 24 | } 25 | 26 | $currentUserName = & git config user.name; 27 | if ($null -eq $currentUserName) { 28 | Write-host "Git user.name empty, setting up a 'service' one..." -ForegroundColor DarkGray; 29 | & git config user.name "(Pipeline Automation)"; 30 | } 31 | 32 | $changesDetected = $false; 33 | 34 | Write-host "Git status:" -ForegroundColor DarkGray; 35 | & git status 36 | 37 | Write-host "Branch:" -ForegroundColor DarkGray; 38 | & git branch 39 | 40 | Write-host "Checking [$outputFile]..." -ForegroundColor DarkGray; 41 | $gitstatus = & git status -s -- $outputFile; 42 | if ($null -ne $gitstatus) 43 | { 44 | Write-host "$outputFile changes detected. Auto-commit..." -ForegroundColor DarkGray; 45 | & git add -- $outputFile; 46 | & git commit -m "resouces.yaml autocommit [skip ci]" $outputFile; 47 | $changesDetected = $true; 48 | } 49 | 50 | Write-host "Checking [$autogeneratedDefinitionsFolder]..." -ForegroundColor DarkGray; 51 | $gitstatus = & git status -s -- $autogeneratedDefinitionsFolder; 52 | if ($null -ne $gitstatus) 53 | { 54 | Write-host "$autogeneratedDefinitionsFolder changes detected. Auto-commit..." -ForegroundColor DarkGray; 55 | & git add -- $autogeneratedDefinitionsFolder; 56 | & git commit -m "backstage/* autocommit [skip ci]" -- $autogeneratedDefinitionsFolder; 57 | $changesDetected = $true; 58 | } 59 | 60 | if ($changesDetected) { 61 | Write-host "Pushing changes to remote..." -ForegroundColor DarkGray; 62 | & git push --quiet; 63 | } else { 64 | Write-host "No changes detected..." -ForegroundColor DarkGray; 65 | } 66 | 67 | Write-host "...done" -ForegroundColor Gray 68 | -------------------------------------------------------------------------------- /plugins/score-card/tools/azure-devops-pipelines/templates/checkout-branch-properly.yaml: -------------------------------------------------------------------------------- 1 | # this will checkout current branch (e.g. main, PR,...) so we can commit changes 2 | # may be part of your template steps in some shared repository 3 | 4 | steps: 5 | - checkout: self #see https://docs.microsoft.com/en-us/azure/devops/pipelines/scripts/git-commands?view=azure-devops&tabs=yaml 6 | displayName: 'Checkout and keep git credentials' 7 | persistCredentials: true 8 | 9 | - task: PowerShell@2.212.0 10 | displayName: 'Checkout branch properly' 11 | inputs: 12 | targetType: 'inline' 13 | script: | 14 | $pullReqestSrcBranch = '$(System.PullRequest.SourceBranch)' 15 | $sourceBranch = '$(Build.SourceBranch)' 16 | if ($pullReqestSrcBranch -ne ('$' + '(System.PullRequest.SourceBranch)')) 17 | { 18 | $sourceBranch = $pullReqestSrcBranch; 19 | } 20 | $sourceBranch = $sourceBranch.Replace('refs/heads/', ''); 21 | Write-Output("pullReqestSrcBranch: [$pullReqestSrcBranch], Build.SourceBranch: $(Build.SourceBranch), sourceBranch: [$sourceBranch]") 22 | git fetch --depth=1 --quiet origin "+refs/heads/$sourceBranch" 23 | git checkout --merge --quiet -B "$sourceBranch" "origin/$sourceBranch" 24 | failOnStderr: true 25 | showWarnings: true 26 | pwsh: true 27 | workingDirectory: '$(Build.SourcesDirectory)' 28 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:js-lib", 5 | ":rebaseStalePrs", 6 | ":automergePatch", 7 | "docker:disable" 8 | ], 9 | "assignees": [ 10 | "jvilimek" 11 | ], 12 | "labels": [ 13 | "dependencies" 14 | ], 15 | "stabilityDays": 3, 16 | "internalChecksFilter": "strict", 17 | "packageRules": [ 18 | { 19 | "paths": [ 20 | "packages/app/package.json" 21 | ], 22 | "addLabels": [ 23 | "packages" 24 | ] 25 | }, 26 | { 27 | "paths": [ 28 | "packages/backend/package.json" 29 | ], 30 | "addLabels": [ 31 | "packages" 32 | ] 33 | }, 34 | { 35 | "description": "In case of eslint package update change the label to [linting]", 36 | "matchPackagePatterns": [ 37 | "eslint" 38 | ], 39 | "labels": [ 40 | "linting" 41 | ] 42 | }, 43 | { 44 | "description": "In case we have optional dependencies change add [optional] label", 45 | "matchDepTypes": [ 46 | "optionalDependencies" 47 | ], 48 | "addLabels": [ 49 | "optional" 50 | ] 51 | } 52 | ] 53 | } -------------------------------------------------------------------------------- /scripts/check-if-release.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | /* 4 | * Copyright 2020 The Backstage Authors 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | // This script is used to determine whether a particular commit has changes 20 | // that should lead to a release. It is run as part of the main master build 21 | // to determine whether the release flow should be run as well. 22 | // 23 | // It has the following output which can be used later in GitHub actions: 24 | // 25 | // needs_release = 'true' | 'false' 26 | 27 | const { execFile: execFileCb } = require('child_process'); 28 | const { resolve: resolvePath } = require('path'); 29 | const { promises: fs } = require('fs'); 30 | const { promisify } = require('util'); 31 | 32 | const parentRef = 33 | !process.env.COMMIT_SHA_BEFORE || 34 | process.env.COMMIT_SHA_BEFORE === '0000000000000000000000000000000000000000' 35 | ? 'HEAD^' 36 | : process.env.COMMIT_SHA_BEFORE; 37 | 38 | const execFile = promisify(execFileCb); 39 | 40 | async function runPlain(cmd, ...args) { 41 | try { 42 | const { stdout } = await execFile(cmd, args, { shell: true }); 43 | return stdout.trim(); 44 | } catch (error) { 45 | if (error.stderr) { 46 | process.stderr.write(error.stderr); 47 | } 48 | if (!error.code) { 49 | throw error; 50 | } 51 | throw new Error( 52 | `Command '${[cmd, ...args].join(' ')}' failed with code ${error.code}`, 53 | ); 54 | } 55 | } 56 | 57 | async function main() { 58 | process.cwd(resolvePath(__dirname, '..')); 59 | 60 | const diff = await runPlain( 61 | 'git', 62 | 'diff', 63 | '--name-only', 64 | parentRef, 65 | '--', 66 | "'*/package.json'", // Git treats this as what would usually be **/package.json 67 | ); 68 | const packageList = diff 69 | .split('\n') 70 | .filter(path => path.match(/^(plugins)\/[^/]+\/package\.json$/)); 71 | 72 | const packageVersions = await Promise.all( 73 | packageList.map(async path => { 74 | let name; 75 | let newVersion; 76 | let oldVersion; 77 | 78 | try { 79 | const data = JSON.parse( 80 | await runPlain('git', 'show', `${parentRef}:${path}`), 81 | ); 82 | name = data.name; 83 | oldVersion = data.version; 84 | } catch { 85 | oldVersion = ''; 86 | } 87 | 88 | try { 89 | const data = JSON.parse(await fs.readFile(path, 'utf8')); 90 | name = data.name; 91 | newVersion = data.version; 92 | } catch (error) { 93 | if (error.code === 'ENOENT') { 94 | newVersion = ''; 95 | } 96 | } 97 | 98 | return { name, oldVersion, newVersion }; 99 | }), 100 | ); 101 | 102 | const newVersions = packageVersions.filter( 103 | ({ oldVersion, newVersion }) => 104 | oldVersion !== newVersion && 105 | oldVersion !== '' && 106 | newVersion !== '', 107 | ); 108 | 109 | if (newVersions.length === 0) { 110 | console.log('No package version bumps detected, no release needed'); 111 | console.log(`::set-output name=needs_release::false`); 112 | return; 113 | } 114 | 115 | console.log('Package version bumps detected, a new release is needed'); 116 | const maxLength = Math.max(...newVersions.map(_ => _.name.length)); 117 | for (const { name, oldVersion, newVersion } of newVersions) { 118 | console.log( 119 | ` ${name.padEnd(maxLength, ' ')} ${oldVersion} to ${newVersion}`, 120 | ); 121 | } 122 | console.log(`::set-output name=needs_release::true`); 123 | } 124 | 125 | main().catch(error => { 126 | console.error(error.stack); 127 | process.exit(1); 128 | }); 129 | -------------------------------------------------------------------------------- /scripts/copyright-header.txt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright <%= YEAR %> <%= NAME %> 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ -------------------------------------------------------------------------------- /scripts/create-release-tag.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | /* 4 | * Copyright 2020 The Backstage Authors 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | const { Octokit } = require('@octokit/rest'); 20 | const path = require('path'); 21 | const fs = require('fs-extra'); 22 | 23 | const baseOptions = { 24 | owner: 'Oriflame', 25 | repo: 'backstage-plugins', 26 | }; 27 | 28 | async function getCurrentReleaseTag() { 29 | const rootPath = path.resolve(__dirname, '../package.json'); 30 | return fs.readJson(rootPath).then(_ => _.version); 31 | } 32 | 33 | async function createGitTag(octokit, commitSha, tagName) { 34 | const annotatedTag = await octokit.git.createTag({ 35 | ...baseOptions, 36 | tag: tagName, 37 | message: tagName, 38 | object: commitSha, 39 | type: 'commit', 40 | }); 41 | 42 | try { 43 | await octokit.git.createRef({ 44 | ...baseOptions, 45 | ref: `refs/tags/${tagName}`, 46 | sha: annotatedTag.data.sha, 47 | }); 48 | } catch (ex) { 49 | if ( 50 | ex.status === 422 && 51 | ex.response.data.message === 'Reference already exists' 52 | ) { 53 | throw new Error(`Tag ${tagName} already exists in repository`); 54 | } 55 | console.error(`Tag creation for ${tagName} failed`); 56 | throw ex; 57 | } 58 | } 59 | 60 | async function main() { 61 | if (!process.env.GITHUB_SHA) { 62 | throw new Error('GITHUB_SHA is not set'); 63 | } 64 | if (!process.env.GITHUB_TOKEN) { 65 | throw new Error('GITHUB_TOKEN is not set'); 66 | } 67 | 68 | const commitSha = process.env.GITHUB_SHA; 69 | const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); 70 | 71 | const releaseVersion = await getCurrentReleaseTag(); 72 | const tagName = `v${releaseVersion}`; 73 | 74 | console.log(`Creating release tag ${tagName} at ${commitSha}`); 75 | await createGitTag(octokit, commitSha, tagName); 76 | 77 | console.log(`::set-output name=tag_name::${tagName}`); 78 | console.log(`::set-output name=version::${releaseVersion}`); 79 | } 80 | 81 | main().catch(error => { 82 | console.error(error.stack); 83 | process.exit(1); 84 | }); 85 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@backstage/cli/config/tsconfig.json", 3 | "include": [ 4 | "packages/**/src", 5 | "plugins/**/src", 6 | "plugins/**/dev", 7 | "plugins/**/migrations" 8 | ], 9 | "exclude": ["node_modules"], 10 | "compilerOptions": { 11 | "outDir": "dist-types", 12 | "rootDir": ".", 13 | "useUnknownInCatchVariables": false 14 | } 15 | } 16 | --------------------------------------------------------------------------------