├── .devcontainer ├── devcontainer.json └── postcreate.sh ├── .dockerignore ├── .eslintrc.json ├── .gitattributes ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug.yaml │ ├── config.yaml │ ├── engineering.yaml │ └── feature.yaml ├── actions │ └── analyze-image │ │ └── action.yaml ├── dependabot.yml ├── pull_request_template.md ├── scripts │ └── get_release_version.py └── workflows │ ├── build.yaml │ └── devops-boards.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── .yarnrc.yml ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── GOVERNANCE.md ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── app-config.dashboard.yaml ├── app-config.local.yaml ├── app-config.yaml ├── backstage.json ├── deploy └── dashboard.yaml ├── docs └── contributing │ ├── contributing-code │ ├── contributing-code-building │ │ └── README.md │ ├── contributing-code-developing │ │ └── README.md │ └── contributing-code-organization │ │ └── README.md │ └── contributing-issues │ └── README.md ├── examples ├── entities.yaml └── org.yaml ├── package.json ├── packages ├── app │ ├── .eslintignore │ ├── .eslintrc.js │ ├── e2e-tests │ │ └── app.test.ts │ ├── package.json │ ├── public │ │ ├── android-icon-144x144.png │ │ ├── android-icon-192x192.png │ │ ├── android-icon-36x36.png │ │ ├── android-icon-48x48.png │ │ ├── android-icon-72x72.png │ │ ├── android-icon-96x96.png │ │ ├── apple-icon-114x114.png │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-144x144.png │ │ ├── apple-icon-152x152.png │ │ ├── apple-icon-180x180.png │ │ ├── apple-icon-57x57.png │ │ ├── apple-icon-60x60.png │ │ ├── apple-icon-72x72.png │ │ ├── apple-icon-76x76.png │ │ ├── apple-icon-precomposed.png │ │ ├── apple-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo-full.svg │ │ ├── manifest.json │ │ ├── ms-icon-144x144.png │ │ ├── ms-icon-150x150.png │ │ ├── ms-icon-310x310.png │ │ ├── ms-icon-70x70.png │ │ ├── robots.txt │ │ └── safari-pinned-tab.svg │ └── src │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── apis.ts │ │ ├── components │ │ ├── Root │ │ │ ├── Root.tsx │ │ │ └── index.ts │ │ └── home │ │ │ ├── CommunityCard.tsx │ │ │ ├── HomePage.tsx │ │ │ ├── LearnCard.tsx │ │ │ └── SupportCard.tsx │ │ ├── index.tsx │ │ └── setupTests.ts ├── backend │ ├── .eslintrc.js │ ├── Dockerfile │ ├── README.md │ ├── package.json │ └── src │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── plugins │ │ ├── app.ts │ │ ├── auth.ts │ │ ├── catalog.ts │ │ ├── kubernetes.ts │ │ └── proxy.ts │ │ └── types.ts └── rad-components │ ├── .storybook │ ├── main.ts │ └── preview.ts │ ├── babel.config.json │ ├── eslintrc │ ├── index.ts │ ├── jest.config.json │ ├── package.json │ └── src │ ├── components │ ├── appgraph │ │ ├── AppGraph.tsx │ │ ├── __docs__ │ │ │ ├── AppGraph.mdx │ │ │ ├── AppGraph.stories.tsx │ │ │ └── Example.tsx │ │ ├── __test__ │ │ │ └── AppGraph.test.tsx │ │ └── index.ts │ ├── index.ts │ └── resourcenode │ │ ├── ResourceNode.tsx │ │ ├── __docs__ │ │ ├── Example.tsx │ │ ├── ResourceNode.mdx │ │ └── ResourceNode.stories.tsx │ │ ├── __test__ │ │ └── ResourceNode.test.tsx │ │ └── index.ts │ ├── graph.ts │ ├── index.ts │ ├── resourceId.ts │ ├── sampledata.ts │ └── setupTests.ts ├── playwright.config.ts ├── plugins ├── README.md ├── plugin-radius-backend │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ └── src │ │ ├── index.ts │ │ ├── run.ts │ │ ├── service │ │ ├── router.test.ts │ │ ├── router.ts │ │ └── standaloneServer.ts │ │ └── setupTests.ts └── plugin-radius │ ├── .eslintrc.js │ ├── README.md │ ├── dev │ └── index.tsx │ ├── index.ts │ ├── package.json │ └── src │ ├── api │ ├── api.test.ts │ ├── api.ts │ └── index.ts │ ├── components │ ├── applications │ │ ├── ApplicationIcon.test.tsx │ │ ├── ApplicationIcon.tsx │ │ ├── ApplicationListInfoCard.tsx │ │ ├── ApplicationListPage.test.tsx │ │ ├── ApplicationListPage.tsx │ │ └── index.ts │ ├── environments │ │ ├── EnvironmentIcon.test.tsx │ │ ├── EnvironmentIcon.tsx │ │ ├── EnvironmentListInfoCard.tsx │ │ ├── EnvironmentListPage.test.tsx │ │ ├── EnvironmentListPage.tsx │ │ └── index.ts │ ├── logo │ │ ├── RadiusLogo.test.tsx │ │ ├── RadiusLogo.tsx │ │ ├── RadiusLogomarkReverse.test.tsx │ │ └── RadiusLogomarkReverse.tsx │ ├── recipes │ │ ├── RecipeIcon.test.tsx │ │ ├── RecipeIcon.tsx │ │ ├── RecipeListPage.tsx │ │ ├── RecipeTable.tsx │ │ └── index.ts │ ├── resourcebreadcrumbs │ │ ├── ResourceBreadcrumbs.test.tsx │ │ ├── ResourceBreadcrumbs.tsx │ │ └── index.ts │ ├── resourcelink │ │ ├── ResourceLink.test.tsx │ │ ├── ResourceLink.tsx │ │ └── index.ts │ ├── resources │ │ ├── ApplicationResourcesTab.tsx │ │ ├── ApplicationTab.tsx │ │ ├── DetailsTab.tsx │ │ ├── EnvironmentResourcesTab.tsx │ │ ├── OverviewTab.tsx │ │ ├── RecipesTab.tsx │ │ ├── ResourceIcon.test.tsx │ │ ├── ResourceIcon.tsx │ │ ├── ResourceLayout.tsx │ │ ├── ResourceListPage.tsx │ │ ├── ResourcePage.test.tsx │ │ ├── ResourcePage.tsx │ │ └── index.ts │ └── resourcetable │ │ ├── ResourceTable.test.tsx │ │ ├── ResourceTable.tsx │ │ └── index.ts │ ├── features.ts │ ├── index.ts │ ├── plugin.test.ts │ ├── plugin.ts │ ├── resources │ ├── index.ts │ ├── resource.ts │ ├── resourceId.test.ts │ └── resourceId.ts │ ├── routes.ts │ └── setupTests.ts ├── tsconfig.json └── yarn.lock /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node 3 | { 4 | "name": "Dashboard", 5 | "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye", 6 | "features": { 7 | "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": { 8 | "version": "latest", 9 | "helm": "latest", 10 | "minikube": "none" 11 | }, 12 | "ghcr.io/rio/features/k3d:1": {}, 13 | "ghcr.io/devcontainers/features/docker-in-docker:2": { 14 | "version": "latest", 15 | "moby": true 16 | } 17 | }, 18 | 19 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 20 | "forwardPorts": [3000, 7007, 8001], 21 | 22 | // Use 'postCreateCommand' to run commands after the container is created. 23 | "postCreateCommand": "./.devcontainer/postcreate.sh", 24 | 25 | // Configure tool-specific properties. 26 | "customizations": { 27 | "vscode": { 28 | "extensions": [ 29 | "dbaeumer.vscode-eslint", 30 | "esbenp.prettier-vscode", 31 | "orta.vscode-jest", 32 | "ms-azuretools.vscode-bicep" 33 | ] 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.devcontainer/postcreate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -eu 3 | 4 | # Install the current version of Radius 5 | wget -q "https://raw.githubusercontent.com/radius-project/radius/main/deploy/install.sh" -O - | /bin/bash 6 | 7 | export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 # Disable corepack download prompt (silent option) 8 | sudo corepack enable 9 | yarn install 10 | yarn playwright install chrome 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .yarn/cache 3 | .yarn/install-state.gz 4 | node_modules 5 | packages/*/src 6 | packages/*/node_modules 7 | plugins 8 | *.local.yaml 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/eslintrc.json", 3 | "root": true, 4 | "ignorePatterns": [ 5 | "dist/", 6 | "dist-types/", 7 | "node_modules/", 8 | "playwright.config.ts" 9 | ], 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/recommended", 13 | "plugin:react/recommended" 14 | ], 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "sourceType": "module" 18 | }, 19 | "plugins": ["@typescript-eslint"], 20 | "settings": { 21 | "react": { 22 | "version": "detect" 23 | } 24 | }, 25 | "rules": { 26 | "@typescript-eslint/triple-slash-reference": 0, 27 | "react/no-children-prop": 0 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # text files use OS defaults on checkout, LF on checkin 2 | * text eol=auto 3 | 4 | # shell scripts always use LF 5 | *.sh text eol=lf 6 | 7 | # svg files are text 8 | *.svg test 9 | 10 | # images are binary 11 | *.png binary -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners are the maintainers and approvers of this repo 2 | * @radius-project/maintainers-dashboard @radius-project/approvers-dashboard -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report broken functionality within Radius Dashboard 3 | title: '' 4 | labels: ['bug'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: '## Bug information' 9 | - type: textarea 10 | attributes: 11 | label: Steps to reproduce 12 | description: How can we recreate this bug? Be specific. 13 | validations: 14 | required: true 15 | - type: textarea 16 | attributes: 17 | label: Observed behavior 18 | description: What you're experiencing that you believe is a bug. 19 | placeholder: | 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | Tip: You can attach images or files by clicking this area to highlight it and then dragging files in. 23 | validations: 24 | required: false 25 | - type: textarea 26 | attributes: 27 | label: Desired behavior 28 | description: What you're expecting to happen. 29 | validations: 30 | required: true 31 | - type: textarea 32 | attributes: 33 | label: Workaround 34 | description: Have you found a workaround to get you unblocked? 35 | validations: 36 | required: false 37 | - type: textarea 38 | attributes: 39 | label: Additional context 40 | description: Add any other context about the problem here. 41 | placeholder: | 42 | Links? References? Anything that will give us more context about the issue you are encountering! 43 | 44 | What browser (Chrome, Firefox, Edge, etc.) are you using to access the dashboard? For example: Chrome Version 121.0.6167.85 (Official Build) (arm64) 45 | 46 | What rad cli version are you running? You can run `rad version` and past the output here. 47 | 48 | Tip: You can attach images or files by clicking this area to highlight it and then dragging files in. 49 | validations: 50 | required: false 51 | - type: checkboxes 52 | attributes: 53 | label: Would you like to support us? 54 | description: Would you like to support us in fixing this bug? 55 | options: 56 | - label: Yes, I would like to support you 57 | required: false 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yaml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Discord Support 4 | url: https://discord.gg/SRG3ePMKNy 5 | about: Please ask any questions you may have here 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/engineering.yaml: -------------------------------------------------------------------------------- 1 | name: Engineering Improvement 2 | description: Report problems or suggestions to improve the Radius Dashboard engineering processes and/or pipelines 3 | title: '' 4 | labels: ['maintenance'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: '## Engineering Improvement' 9 | - type: textarea 10 | attributes: 11 | label: Area for Improvement 12 | description: What engineering process or tools can be improved? Build? Testing? ...? Be specific. 13 | validations: 14 | required: true 15 | - type: textarea 16 | attributes: 17 | label: Observed behavior 18 | description: What you're experiencing that you believe could be improved. 19 | placeholder: | 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | Tip: You can attach images or files by clicking this area to highlight it and then dragging files in. 23 | validations: 24 | required: false 25 | - type: textarea 26 | attributes: 27 | label: Desired behavior 28 | description: What you'd like to happen. 29 | validations: 30 | required: true 31 | - type: textarea 32 | attributes: 33 | label: Proposed Fix 34 | description: Have you found a way to implement or fix the issue? 35 | validations: 36 | required: false 37 | - type: textarea 38 | attributes: 39 | label: Additional context 40 | description: Add any other context about the problem here. 41 | placeholder: | 42 | Links? References? Anything that will give us more context about the issue you are encountering! 43 | 44 | What browser (Chrome, Firefox, Edge, etc.) are you using to access the dashboard? For example: Chrome Version 121.0.6167.85 (Official Build) (arm64) 45 | 46 | What rad cli version are you running? You can run `rad version` and past the output here. 47 | 48 | Tip: You can attach images or files by clicking this area to highlight it and then dragging files in. 49 | validations: 50 | required: false 51 | - type: checkboxes 52 | attributes: 53 | label: Would you like to support us? 54 | description: Would you like to support us in improving Radius engineering processes and/or pipelines? 55 | options: 56 | - label: Yes, I would like to support you 57 | required: false 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yaml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request a feature in Radius Dashboard 3 | title: '<FEATURE TITLE>' 4 | labels: ['feature'] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Overview of feature request 9 | description: What are you proposing Radius Dashboard add/update/remove? 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: Acceptance criteria 15 | description: What will need to be completed/working for this feature to be marked "Done"? 16 | validations: 17 | required: false 18 | - type: textarea 19 | attributes: 20 | label: Additional context 21 | description: Add any other context about the problem here 22 | placeholder: | 23 | Links? References? Anything that will give us more context about the feature you are looking for! 24 | 25 | Tip: You can attach images or files by clicking this area to highlight it and then dragging files in. 26 | validations: 27 | required: false 28 | - type: checkboxes 29 | attributes: 30 | label: Would you like to support us? 31 | description: Would you like to support us in implementing the feature? 32 | options: 33 | - label: Yes, I would like to support you 34 | required: false 35 | -------------------------------------------------------------------------------- /.github/actions/analyze-image/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'Analyze Image (dive)' 2 | description: | 3 | Analyze a container image using dive (https://github.com/wagoodman/dive). 4 | 5 | This will output statistics about the layers, size, and wasted space in the image. 6 | inputs: 7 | image: 8 | description: 'Image to analyze' 9 | required: true 10 | docker-socket: 11 | description: 'Path to the docker socket' 12 | required: false 13 | default: '/var/run/docker.sock' 14 | outputs: {} 15 | runs: 16 | using: 'composite' 17 | steps: 18 | - name: Analyze with Dive 19 | shell: bash 20 | run: | 21 | docker run \ 22 | --rm \ 23 | -v ${{ inputs.docker-socket }}:/var/run/docker.sock \ 24 | -e CI=true \ 25 | wagoodman/dive:latest \ 26 | ${{ inputs.image }} 27 | - name: Print (uncompressed) total size 28 | shell: bash 29 | run: | 30 | docker inspect -f "{{ .Size }}" ${{ inputs.image }} | awk '{ byte =$1 /1024/1024; print byte " MB" }' 31 | - name: Print layers 32 | shell: bash 33 | run: | 34 | docker history ${{ inputs.image }} 35 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | groups: 8 | all: 9 | patterns: 10 | - '*' 11 | - package-ecosystem: 'npm' 12 | directory: '/' 13 | schedule: 14 | interval: 'weekly' 15 | groups: 16 | all: 17 | patterns: 18 | - '*' 19 | - package-ecosystem: 'devcontainers' 20 | directory: '/.devcontainer' 21 | schedule: 22 | interval: weekly 23 | groups: 24 | all: 25 | patterns: 26 | - '*' 27 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | _Please explain the changes you've made._ 4 | 5 | ## Type of change 6 | 7 | <!-- 8 | 9 | Please select **one** of the following options that describes your change and delete the others. Clearly identifying the type of change you are making will help us review your PR faster, and is used in authoring release notes. 10 | 11 | If you are making a bug fix or functionality change to Radius Dashboard and do not have an associated issue link please create one now. 12 | 13 | --> 14 | 15 | - This pull request fixes a bug in Radius Dashboard and has an approved issue (issue link required). 16 | - This pull request adds or changes features of Radius Dashboard and has an approved issue (issue link required). 17 | - This pull request is a minor refactor, code cleanup, test improvement, or other maintenance task and doesn't change the functionality of Radius Dashboard (issue link optional). 18 | 19 | <!-- 20 | 21 | Please update the following to link the associated issue. This is required for some kinds of changes (see above). 22 | 23 | --> 24 | 25 | Fixes: #issue_number 26 | 27 | ## Screenshots 28 | 29 | <!-- 30 | 31 | Please attach screenshots of UI resulting from the changes. 32 | 33 | --> -------------------------------------------------------------------------------- /.github/scripts/get_release_version.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------ 2 | # Copyright 2023 The Radius 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 | # This script parses release version from Git tag and set the parsed version to 18 | # environment variable, REL_VERSION. 19 | 20 | # We set the environment variable REL_CHANNEL based on the kind of build. This is used for 21 | # versioning of our assets. 22 | # 23 | # REL_CHANNEL is: 24 | # 'edge': for most builds 25 | # 'edge': for PR builds 26 | # '1.0.0-rc1' (the full version): for a tagged prerelease 27 | # '1.0' (major.minor): for a tagged release 28 | 29 | # We set the environment variable UPDATE_RELEASE if it's a full release (tagged and not prerelease) 30 | 31 | # REL_VERSION is used to stamp versions into binaries 32 | # REL_CHANNEL is used to upload assets to different paths 33 | 34 | # This way a 1.0 user can download 1.0.1, etc. 35 | 36 | import os 37 | import re 38 | import sys 39 | 40 | gitRef = os.getenv("GITHUB_REF") 41 | 42 | # From https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string 43 | # Group 'version' returns the whole version 44 | # other named groups return the components 45 | tagRefRegex = r"^refs/tags/v(?P<version>0|(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)$" 46 | pullRefRegex = r"^refs/pull/(.*)/(.*)$" 47 | 48 | with open(os.getenv("GITHUB_ENV"), "a") as githubEnv: 49 | if gitRef is None: 50 | print("This is not running in github, GITHUB_REF is null. Assuming a local build...") 51 | 52 | version = "REL_VERSION=edge" 53 | print("Setting: {}".format(version)) 54 | githubEnv.write(version + "\n") 55 | 56 | channel = "REL_CHANNEL=edge" 57 | print("Setting: {}".format(channel)) 58 | githubEnv.write(channel + "\n") 59 | 60 | tag = "REL_TAG=local" 61 | print("Setting: {}".format(tag)) 62 | githubEnv.write(tag + "\n") 63 | 64 | sys.exit(0) 65 | 66 | match = re.search(pullRefRegex, gitRef) 67 | if match is not None: 68 | print("This is pull request {}...".format(match.group(1))) 69 | 70 | version = "REL_VERSION=pr-{}".format(match.group(1)) 71 | print("Setting: {}".format(version)) 72 | githubEnv.write(version + "\n") 73 | 74 | channel = "REL_CHANNEL=edge" 75 | print("Setting: {}".format(channel)) 76 | githubEnv.write(channel + "\n") 77 | 78 | tag = "REL_TAG=pr-{}".format(match.group(1)) 79 | print("Setting: {}".format(tag)) 80 | githubEnv.write(tag + "\n") 81 | 82 | sys.exit(0) 83 | 84 | match = re.search(tagRefRegex, gitRef) 85 | if match is not None: 86 | print("This is tagged as {}...".format(match.group("version"))) 87 | 88 | if match.group("prerelease") is None: 89 | print("This is a full release...") 90 | 91 | version = "REL_VERSION={}".format(match.group("version")) 92 | print("Setting: {}".format(version)) 93 | githubEnv.write(version + "\n") 94 | 95 | channel = "REL_CHANNEL={}.{}".format(match.group("major"), match.group("minor")) 96 | print("Setting: {}".format(channel)) 97 | githubEnv.write(channel + "\n") 98 | 99 | tag = "REL_TAG={}".format(match.group("version")) 100 | print("Setting: {}".format(tag)) 101 | githubEnv.write(tag + "\n") 102 | 103 | print("Setting: UPDATE_RELEASE=true") 104 | githubEnv.write("UPDATE_RELEASE=true" + "\n") 105 | 106 | sys.exit(0) 107 | 108 | else: 109 | print("This is a prerelease...") 110 | 111 | version = "REL_VERSION={}".format(match.group("version")) 112 | print("Setting: {}".format(version)) 113 | githubEnv.write(version + "\n") 114 | 115 | channel = "REL_CHANNEL={}".format(match.group("version")) 116 | print("Setting: {}".format(channel)) 117 | githubEnv.write(channel + "\n") 118 | 119 | tag = "REL_TAG={}".format(match.group("version")) 120 | print("Setting: {}".format(tag)) 121 | githubEnv.write(tag + "\n") 122 | 123 | sys.exit(0) 124 | 125 | print("This is a normal build") 126 | version = "REL_VERSION=edge" 127 | print("Setting: {}".format(version)) 128 | githubEnv.write(version + "\n") 129 | 130 | channel = "REL_CHANNEL=edge" 131 | print("Setting: {}".format(channel)) 132 | githubEnv.write(channel + "\n") 133 | 134 | tag = "REL_TAG=latest" 135 | print("Setting: {}".format(tag)) 136 | githubEnv.write(tag + "\n") 137 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - v* 8 | pull_request: 9 | branches: 10 | - main 11 | permissions: 12 | contents: read 13 | pull-requests: read 14 | issues: read 15 | packages: write 16 | env: 17 | CI_LINT: ${{ github.event_name == 'pull_request' }} 18 | CI_TEST: ${{ github.event_name == 'pull_request' }} 19 | CI_PUBLISH_RELEASE: ${{ github.repository == 'radius-project/dashboard' && startsWith(github.ref, 'refs/tags/v') && github.event_name == 'push' }} 20 | CI_PUBLISH_LATEST: ${{ github.repository == 'radius-project/dashboard' && github.ref == 'refs/heads/main' && github.event_name == 'push' }} 21 | jobs: 22 | build: 23 | name: Build Packages 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout code 27 | uses: actions/checkout@v4 28 | - name: Parse release version and set environment variables 29 | run: python ./.github/scripts/get_release_version.py 30 | - name: Enable corepack 31 | run: corepack enable 32 | - name: Install Node.js 21 # Must be after corepack is enabled. 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: '21' 36 | cache: 'yarn' 37 | cache-dependency-path: 'yarn.lock' 38 | - name: Install dependencies 39 | run: yarn install --frozen-lockfile 40 | - name: Lint 41 | if: ${{ env.CI_LINT == 'true' }} 42 | run: yarn run lint:all 43 | - name: Format 44 | if: ${{ env.CI_LINT == 'true' }} 45 | run: yarn run format:check 46 | - name: Build TypeScript 47 | run: yarn run tsc 48 | - name: Build 49 | run: yarn workspaces foreach -A run build:all 50 | - name: Build Storybook 51 | run: yarn workspace @radapp.io/rad-components run build-storybook 52 | - name: Run Tests 53 | if: ${{ env.CI_TEST == 'true' }} 54 | run: yarn run test:all 55 | - name: Run E2E Tests 56 | if: ${{ env.CI_TEST == 'true' }} 57 | run: yarn run test:e2e 58 | build-and-publish-container: 59 | name: Build and Publish Container 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Checkout code 63 | uses: actions/checkout@v4 64 | - name: Parse release version and set environment variables 65 | run: python ./.github/scripts/get_release_version.py 66 | - name: Enable corepack 67 | run: corepack enable 68 | - name: Install Node.js 21 # Must be after corepack is enabled. 69 | uses: actions/setup-node@v4 70 | with: 71 | node-version: '21' 72 | cache: 'yarn' 73 | cache-dependency-path: 'yarn.lock' 74 | - name: Install dependencies 75 | run: yarn install --frozen-lockfile 76 | - name: Build TypeScript 77 | run: yarn run tsc 78 | - name: Build Image 79 | run: yarn build:backend --config ../../app-config.yaml --config ../../app-config.dashboard.yaml 80 | - name: Build Image 81 | run: yarn build-image 82 | - name: Analyze Image 83 | uses: ./.github/actions/analyze-image 84 | with: 85 | image: ghcr.io/radius-project/dashboard:latest 86 | - name: Login to ghcr.io 87 | if: ${{ env.CI_PUBLISH_LATEST == 'true' || env.CI_PUBLISH_RELEASE == 'true' }} 88 | uses: docker/login-action@v3 89 | with: 90 | registry: ghcr.io 91 | username: ${{ github.actor }} 92 | password: ${{ secrets.GITHUB_TOKEN }} 93 | - name: Push Image to ghcr.io (push to main) 94 | if: ${{ env.CI_PUBLISH_LATEST == 'true' }} 95 | run: | 96 | docker push ghcr.io/radius-project/dashboard:latest 97 | - name: Push Image to ghcr.io (push to tag) 98 | if: ${{ env.CI_PUBLISH_RELEASE == 'true' }} 99 | run: | 100 | docker tag ghcr.io/radius-project/dashboard:latest ghcr.io/radius-project/dashboard:${{ env.REL_CHANNEL }} 101 | docker push ghcr.io/radius-project/dashboard:${{ env.REL_CHANNEL }} 102 | -------------------------------------------------------------------------------- /.github/workflows/devops-boards.yaml: -------------------------------------------------------------------------------- 1 | name: Sync issue to Azure DevOps work item 2 | 3 | on: 4 | issues: 5 | types: 6 | [opened, edited, deleted, closed, reopened, labeled, unlabeled, assigned] 7 | 8 | concurrency: 9 | group: issue-${{ github.event.issue.number }} 10 | cancel-in-progress: false 11 | 12 | # Extra permissions needed to login with Entra ID service principal via federated identity 13 | permissions: 14 | id-token: write 15 | issues: write 16 | 17 | jobs: 18 | ado: 19 | runs-on: ubuntu-latest 20 | environment: 21 | name: issues 22 | steps: 23 | # Auth using Azure Service Principals was added as a part of v2.3 24 | # reference: https://github.com/danhellem/github-actions-issue-to-work-item/pull/143 25 | - name: Login to Azure 26 | uses: azure/login@v2 27 | with: 28 | client-id: ${{ vars.AZURE_SP_DEVOPS_SYNC_CLIENT_ID }} 29 | tenant-id: ${{ vars.AZURE_SP_DEVOPS_SYNC_TENANT_ID }} 30 | allow-no-subscriptions: true 31 | - name: Get Azure DevOps token 32 | id: get_ado_token 33 | run: 34 | # The resource ID for Azure DevOps is always 499b84ac-1321-427f-aa17-267ca6975798 35 | # https://learn.microsoft.com/azure/devops/integrate/get-started/authentication/service-principal-managed-identity 36 | echo "ADO_TOKEN=$(az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query "accessToken" --output tsv)" >> $GITHUB_ENV 37 | - name: Sync issue to Azure DevOps 38 | uses: danhellem/github-actions-issue-to-work-item@v2.4 39 | env: 40 | ado_token: ${{ env.ADO_TOKEN }} 41 | github_token: '${{ secrets.GH_RAD_CI_BOT_PAT }}' 42 | ado_organization: 'azure-octo' 43 | ado_project: 'Incubations' 44 | ado_area_path: "Incubations\\Radius" 45 | ado_iteration_path: "Incubations\\Radius" 46 | ado_new_state: 'New' 47 | ado_active_state: 'Active' 48 | ado_close_state: 'Closed' 49 | ado_wit: 'GitHub Issue' 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Coverage directory generated when running tests with coverage 13 | coverage 14 | 15 | # Dependencies 16 | node_modules/ 17 | 18 | # Yarn 3 files 19 | .pnp.* 20 | .yarn/* 21 | !.yarn/patches 22 | !.yarn/plugins 23 | !.yarn/releases 24 | !.yarn/sdks 25 | !.yarn/versions 26 | 27 | # Node version directives 28 | .nvmrc 29 | 30 | # dotenv environment variables file 31 | .env 32 | .env.test 33 | 34 | # Build output 35 | dist/ 36 | dist-types/ 37 | storybook-static/ 38 | logs/ 39 | 40 | # Temporary change files created by Vim 41 | *.swp 42 | 43 | # MkDocs build output 44 | site 45 | 46 | # Local configuration files 47 | *.local.yaml 48 | 49 | # Sensitive credentials 50 | *-credentials.yaml 51 | 52 | # vscode database functionality support files 53 | *.session.sql 54 | 55 | # E2E test reports 56 | e2e-test-report/ 57 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist-types 3 | coverage 4 | .vscode 5 | node_modules/ 6 | storybook-static/ 7 | **/*.md 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 160, 3 | "tabWidth": 2 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "orta.vscode-jest"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "[json]": { 4 | "editor.tabSize": 2 5 | }, 6 | "[jsonc]": { 7 | "editor.tabSize": 2 8 | }, 9 | "[javascript]": { 10 | "editor.tabSize": 2 11 | }, 12 | "[javascriptreact]": { 13 | "editor.tabSize": 2 14 | }, 15 | "[typescript]": { 16 | "editor.tabSize": 2 17 | }, 18 | "[typescriptreact]": { 19 | "editor.tabSize": 2 20 | }, 21 | "jest.jestCommandLine": "yarn run test:all", 22 | "jest.virtualFolders": [ 23 | { "name": "unit-tests", "jestCommandLine": "yarn run test:all" }, 24 | { "name": "e2e-tests", "jestCommandLine": "yarn run test:e2e" } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # See the owners for this repo at .github/CODEOWNERS -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Radius Community Code of Conduct 2 | 3 | Please refer to our [Radius Community Code of Conduct](https://github.com/radius-project/community/blob/main/CODE-OF-CONDUCT.md) 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Radius is in an early phase of development right now. We welcome feedback in the form of issues that comes from usage and is aligned with the current scope and goals of the project. 4 | 5 | ## Table of contents 6 | 7 | You can find our full **contributor documentation** including instructions at the following links: 8 | 9 | - [Code organization](docs/contributing/contributing-code/contributing-code-organization/) 10 | - [Code building](docs/contributing/contributing-code/contributing-code-building/) 11 | - [Code developing](docs/contributing/contributing-code/contributing-code-developing/) 12 | - [Contributing issues](docs/contributing/contributing-issues/) 13 | 14 | ## Current status 15 | 16 | We welcome small pull request contributions from anyone (docs improvements, bug fixes, minor features.) as long as they follow a few guidelines: 17 | 18 | - For very minor changes like correcting a typo feel free to send a pull request. Otherwise ... 19 | - Please start by [choosing an existing issue](https://github.com/radius-project/dashboard/issues), or [opening an issue](https://github.com/radius-project/dashboard/issues/new/choose) to work on. 20 | - The maintainers will respond to your issue, please work with the maintainers to ensure that what you're doing is in scope for the project before writing any code. 21 | - If you have any doubt whether a contribution would be valuable, feel free to ask. 22 | 23 | ## Developer Certificate of Origin 24 | 25 | The Radius project follows the [Developer Certificate of Origin](https://developercertificate.org/). This is a lightweight way for contributors to certify that they wrote or otherwise have the right to submit the code they are contributing to the project. 26 | 27 | Contributors sign-off that they adhere to these requirements by adding a Signed-off-by line to commit messages. 28 | 29 | ``` 30 | This is my commit message 31 | 32 | Signed-off-by: Random J Developer <random@developer.example.org> 33 | ``` 34 | 35 | Git even has a -s command line option to append this automatically to your commit message: 36 | 37 | ``` 38 | $ git commit -s -m 'This is my commit message' 39 | ``` 40 | 41 | Visual Studio Code has a setting, `git.alwaysSignOff` to automatically add a Signed-off-by line to commit messages. Search for "sign-off" in VS Code settings to find it and enable it. 42 | 43 | ## Code of conduct 44 | 45 | This project has adopted the [Contributor Covenant](http://contributor-covenant.org/). 46 | For more information see [CODE_OF_CONDUCT.md](https://github.com/radius-project/community/blob/main/CODE-OF-CONDUCT.md) 47 | -------------------------------------------------------------------------------- /GOVERNANCE.md: -------------------------------------------------------------------------------- 1 | # Governance 2 | 3 | ## Code of Conduct 4 | 5 | This project has adopted the [Contributor Covenant Code of Conduct](https://github.com/radius-project/community/blob/main/CODE-OF-CONDUCT.md). 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Radius Dashboard 2 | 3 | Radius Dashboard is the frontend experience for [Radius](https://github.com/radius-project/radius), a cloud-native application platform that enables developers and the platform engineers that support them to collaborate on delivering and managing cloud-native applications that follow organizational best practices for cost, operations and security, by default. Radius is an open-source project that supports deploying applications across private cloud, Microsoft Azure, and Amazon Web Services, with more cloud providers to come. 4 | 5 | > NOTE: Radius Dashboard is currently in a prototype stage and thus is not yet packaged into Radius and its releases, though we are planning to add it to the Radius installation soon. The best way to use Radius Dashboard right now is to [manually install it in your cluster](#kubernetes-installation), or clone the repo and run it locally. See the [contribution guide](./CONTRIBUTING.md) for instructions on how to build and run the code. 6 | 7 | The Radius Dashboard is built on [Backstage](https://backstage.io/), an open-source platform for building developer portals that provides a rich set of components to accelerate UI development. The Radius Dashboard is a skinned deployment of Backstage that includes a set of plugins that provide the Radius experience. The components that make up the dashboard are built with extensibility in mind so that they can be used in other contexts beyond Backstage in the future. 8 | 9 | Key features of the Radius Dashboard currently include: 10 | 11 | - _Application graph visualization_: A visualization of the application graph that shows how resources within an application are connected to each other and the underlying infrastructure. 12 | - _Resource overview and details_: Detailed information about resources within Radius, including applications, environments, and infrastructure. 13 | - _Recipes directory_: A listing of all the Radius Recipes available to the user for a given environment. 14 | 15 | ## Kubernetes installation 16 | 17 | > NOTE: The Radius Dashboard is currently in a prototype stage and is distributed separately from the main Radius project. The best way to use Radius Dashboard right now is to manually install it in your cluster: 18 | 19 | 1. Make sure you have a Kubernetes cluster running and `kubectl` installed and configured to point to your cluster. 20 | 1. Ensure you have Radius installed and running in your cluster. If you don't have Radius installed, you can follow the [Radius installation guide](https://docs.radapp.io/installation/) 21 | 1. Apply the [Radius Dashboard manifest](./deploy/dashboard.yaml) to your cluster: 22 | 23 | ```bash 24 | kubectl apply -f https://raw.githubusercontent.com/radius-project/dashboard/main/deploy/dashboard.yaml 25 | ``` 26 | 27 | 1. Once the manifest is applied and the resources are created, port-forward the dashboard service to your local machine: 28 | 29 | ```bash 30 | kubectl port-forward --namespace=radius-system svc/dashboard 3000:80 31 | ``` 32 | 33 | 1. Access the dashboard at [http://localhost:3000](http://localhost:3000) 34 | 35 | ## Getting help 36 | 37 | - ❓ **Have a question?** - Visit our [Discord server](https://discord.gg/SRG3ePMKNy) to post your question and we'll get back to you ASAP 38 | - ⚠️ **Found an issue?** - Refer to our [Issues guide](docs/contributing/contributing-issues) for instructions on filing a bug report 39 | - 💡 **Have a proposal?** - Refer to our [Issues guide](docs/contributing/contributing-issues) for instructions on filing a feature request 40 | 41 | ## Contributing to Radius Dashboard 42 | 43 | Visit [Contributing](./CONTRIBUTING.md) for more information on how to contribute to Radius Dashboard. 44 | 45 | ## Community 46 | 47 | We welcome your contributions and suggestions! One of the easiest ways to contribute is to participate in Issue discussions, chat on [Discord server](https://discord.gg/SRG3ePMKNy) or the monthly [community calls](#community-calls). For more information on the community engagement, developer and contributing guidelines and more, head over to the [Radius community repo](https://github.com/radius-project/community). 48 | 49 | ## Repositories 50 | 51 | [Dashboard](https://github.com/radius-project/dashboard) is the Radius Dashboard repository. It contains all of Dashboard code and documentation. In addition, the Radius project has the below repositories: 52 | 53 | | Repo | Description | 54 | | :------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | 55 | | [Radius](https://github.com/radius-project/radius) | This is the main Radius repository that contains the source code for core Radius. | 56 | | [Docs](https://github.com/radius-project/docs) | This repository contains the Radius documentation source for Radius. | 57 | | [Samples](https://github.com/radius-project/samples) | This repository contains the source code for quickstarts, reference apps, and tutorials for Radius. | 58 | | [Recipes](https://github.com/radius-project/recipes) | This repo contains commonly used Recipe templates for Radius Environments. | 59 | | [Website](https://github.com/radius-project/website) | This repository contains the source code for the Radius website. | 60 | | [Bicep](https://github.com/radius-project/bicep) | This repository contains source code for Bicep, which is a DSL for deploying cloud resources types. | 61 | | [AWS Bicep Types](https://github.com/radius-project/bicep-types-aws) | This repository contains the tooling for Bicep support for AWS resource types. | 62 | 63 | ## Security 64 | 65 | Please refer to our guide on [Reporting security vulnerabilities](https://github.com/radius-project/radius/blob/main/SECURITY.md). 66 | 67 | ## Code of conduct 68 | 69 | Please refer to our [Radius Community Code of Conduct](https://github.com/radius-project/community/blob/main/CODE-OF-CONDUCT.md) 70 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | The Radius maintainers take the security of our project seriously, which includes all source code repositories managed through our GitHub organization. 4 | 5 | If you believe you have found a security vulnerability in any Radius repository, please report it to us as described below. 6 | 7 | ## Reporting Security Issues 8 | 9 | **Please do not report security vulnerabilities through public GitHub issues.** 10 | 11 | Instead, please report them to the [security@radapp.dev](mailto:security@radapp.dev). 12 | 13 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. 14 | 15 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 16 | 17 | - Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 18 | - Full paths of source file(s) related to the manifestation of the issue 19 | - The location of the affected source code (tag/branch/commit or direct URL) 20 | - Any special configuration required to reproduce the issue 21 | - Step-by-step instructions to reproduce the issue 22 | - Proof-of-concept or exploit code (if possible) 23 | - Impact of the issue, including how an attacker might exploit the issue 24 | 25 | This information will help us triage your report more quickly. 26 | 27 | ## Preferred Languages 28 | 29 | We prefer all communications to be in English. 30 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | For help and questions about using this project, please join and post to the [Radius Discord server](https://aka.ms/radius/discord). 6 | 7 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new Issue. 8 | 9 | ## Support Policy 10 | 11 | Support for this project is limited to the resources listed above. 12 | -------------------------------------------------------------------------------- /app-config.dashboard.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 | # Note that the baseUrl should be the URL that the browser and other clients 7 | # should use when communicating with the backend, i.e. it needs to be 8 | # reachable not just from within the backend host, but from all of your 9 | # callers. When its value is "http://localhost:7007", it's strictly private 10 | # and can't be reached by others. 11 | baseUrl: http://localhost:7007 12 | # The listener can also be expressed as a single <host>:<port> string. In this case we bind to 13 | # all interfaces, the most permissive setting. The right value depends on your specific deployment. 14 | listen: ':7007' 15 | 16 | kubernetes: 17 | serviceLocatorMethod: 18 | type: singleTenant 19 | # Use the local proxy on localhost:8001 to talk to the local Kubernetes cluster. 20 | clusterLocatorMethods: 21 | - type: config 22 | clusters: 23 | - name: self 24 | # The URL to the in-cluster Kubernetes API server. 25 | # Backstage docs state it should be ignored when in-cluster, but it appears to be used. 26 | url: https://kubernetes.default.svc.cluster.local 27 | authProvider: serviceAccount 28 | skipTLSVerify: true 29 | skipMetricsLookup: true 30 | catalog: 31 | # Overrides the default list locations from app-config.yaml as these contain example data. 32 | # See https://backstage.io/docs/features/software-catalog/#adding-components-to-the-catalog for more details 33 | # on how to get entities into the catalog. 34 | locations: [] 35 | -------------------------------------------------------------------------------- /app-config.local.yaml: -------------------------------------------------------------------------------- 1 | # Backstage override configuration for your local development environment 2 | # 3 | # This will be used with `yarn dev` 4 | # 5 | # The configuration provided here is meant to be used with a local copy of the dashboard 6 | # talking to a local Kubernetes cluster on localhost. The local Kubernetes cluster should 7 | # have Radius installed. 8 | # 9 | # Use `kubectl proxy` to open a proxy to your local Kubernetes cluster. 10 | 11 | # See: https://backstage.io/docs/conf/writing for the file as a whole. 12 | 13 | # See: https://backstage.io/docs/features/kubernetes/configuration/ 14 | kubernetes: 15 | # Configure backstage to support multiple kubernetes clusters, even though 16 | # only one is used here. 17 | serviceLocatorMethod: 18 | type: multiTenant 19 | # Use the local proxy on localhost:8001 to talk to the local Kubernetes cluster. 20 | clusterLocatorMethods: 21 | - type: localKubectlProxy 22 | 23 | backend: 24 | # Allow the backend to make requests to the local Kubernetes cluster. 25 | reading: 26 | allow: 27 | - host: localhost:8001 28 | - host: 127.0.0.1:8001 29 | -------------------------------------------------------------------------------- /app-config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | title: Radius Dashboard 3 | baseUrl: http://localhost:3000 4 | 5 | organization: 6 | name: Radius 7 | 8 | backend: 9 | # Used for enabling authentication, secret is shared by all backend plugins 10 | # See https://backstage.io/docs/auth/service-to-service-auth for 11 | # information on the format 12 | # auth: 13 | # keys: 14 | # - secret: ${BACKEND_SECRET} 15 | baseUrl: http://localhost:7007 16 | listen: 17 | port: 7007 18 | # Uncomment the following host directive to bind to specific interfaces 19 | # host: 127.0.0.1 20 | csp: 21 | connect-src: ["'self'", 'http:', 'https:'] 22 | # Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference 23 | # Default Helmet Content-Security-Policy values can be removed by setting the key to false 24 | cors: 25 | origin: http://localhost:3000 26 | methods: [GET, HEAD, PATCH, POST, PUT, DELETE] 27 | credentials: true 28 | # This is for local development only, it is not recommended to use this in production 29 | # The production database configuration is stored in app-config.production.yaml 30 | database: 31 | client: better-sqlite3 32 | connection: ':memory:' 33 | # workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir 34 | 35 | integrations: 36 | github: 37 | - host: github.com 38 | # This is a Personal Access Token or PAT from GitHub. You can find out how to generate this token, and more information 39 | # about setting up the GitHub integration here: https://backstage.io/docs/getting-started/configuration#setting-up-a-github-integration 40 | token: ${GITHUB_TOKEN} 41 | ### Example for how to add your GitHub Enterprise instance using the API: 42 | # - host: ghe.example.net 43 | # apiBaseUrl: https://ghe.example.net/api/v3 44 | # token: ${GHE_TOKEN} 45 | 46 | proxy: 47 | {} 48 | ### Example for how to add a proxy endpoint for the frontend. 49 | ### A typical reason to do this is to handle HTTPS and CORS for internal services. 50 | # endpoints: 51 | # '/test': 52 | # target: 'https://example.com' 53 | # changeOrigin: true 54 | 55 | # Reference documentation http://backstage.io/docs/features/techdocs/configuration 56 | # Note: After experimenting with basic setup, use CI/CD to generate docs 57 | # and an external cloud storage when deploying TechDocs for production use-case. 58 | # https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach 59 | techdocs: 60 | builder: 'local' # Alternatives - 'external' 61 | generator: 62 | runIn: 'docker' # Alternatives - 'local' 63 | publisher: 64 | type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives. 65 | 66 | auth: 67 | # see https://backstage.io/docs/auth/ to learn about auth providers 68 | providers: {} 69 | 70 | scaffolder: 71 | {} 72 | # see https://backstage.io/docs/features/software-templates/configuration for software template options 73 | 74 | catalog: 75 | processingInterval: { seconds: 30 } 76 | import: 77 | entityFilename: catalog-info.yaml 78 | pullRequestBranchName: backstage-integration 79 | rules: 80 | - allow: [Component, System, API, Resource, Location] 81 | locations: 82 | # Local example data, file locations are relative to the backend process, typically `packages/backend` 83 | - type: file 84 | target: ../../examples/entities.yaml 85 | 86 | # Local example organizational data 87 | - type: file 88 | target: ../../examples/org.yaml 89 | rules: 90 | - allow: [User, Group] 91 | - type: radius 92 | target: http://127.0.0.1:8001/apis/api.ucp.dev/v1alpha3 93 | 94 | ## Uncomment these lines to add more example data 95 | # - type: url 96 | # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml 97 | 98 | ## Uncomment these lines to add an example org 99 | # - type: url 100 | # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml 101 | # rules: 102 | # - allow: [User, Group] 103 | -------------------------------------------------------------------------------- /backstage.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.21.1" 3 | } 4 | -------------------------------------------------------------------------------- /deploy/dashboard.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: dashboard 5 | namespace: radius-system 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: dashboard 11 | template: 12 | metadata: 13 | labels: 14 | app: dashboard 15 | spec: 16 | serviceAccountName: dashboard-account 17 | containers: 18 | - name: dashboard 19 | image: ghcr.io/radius-project/dashboard:latest 20 | imagePullPolicy: Always 21 | ports: 22 | - name: http 23 | containerPort: 7007 24 | 25 | --- 26 | apiVersion: v1 27 | kind: Service 28 | metadata: 29 | name: dashboard 30 | namespace: radius-system 31 | spec: 32 | selector: 33 | app: dashboard 34 | ports: 35 | - name: http 36 | port: 80 37 | targetPort: http 38 | --- 39 | apiVersion: v1 40 | kind: ServiceAccount 41 | metadata: 42 | name: dashboard-account 43 | namespace: radius-system 44 | --- 45 | apiVersion: rbac.authorization.k8s.io/v1 46 | kind: ClusterRole 47 | metadata: 48 | name: dashboard-role 49 | namespace: radius-system 50 | rules: 51 | - apiGroups: ['', 'api.ucp.dev'] 52 | resources: ['*'] 53 | verbs: ['*'] 54 | --- 55 | apiVersion: rbac.authorization.k8s.io/v1 56 | kind: ClusterRoleBinding 57 | metadata: 58 | name: dashboard-role-binding 59 | namespace: radius-system 60 | subjects: 61 | - kind: ServiceAccount 62 | name: dashboard-account 63 | namespace: radius-system 64 | roleRef: 65 | kind: ClusterRole 66 | name: dashboard-role 67 | apiGroup: rbac.authorization.k8s.io 68 | -------------------------------------------------------------------------------- /docs/contributing/contributing-code/contributing-code-building/README.md: -------------------------------------------------------------------------------- 1 | # Building the Radius Dashboard 2 | 3 | ## Prerequisites 4 | 5 | - Install a modern version of [Node.js](https://nodejs.org/en/download). We use v21.X.X but other versions are ok. 6 | - Enable corepack with `corepack enable`. 7 | 8 | You'll also want an [environment](https://docs.radapp.io/guides/deploy-apps/environments/overview/) where you can experiment with Radius. This means you need to have a Kubernetes cluster where Radius is installed. The Dashboard interacts with the Radius API to access the data it displays. 9 | 10 | For development, we recommend VS Code. This repo is configured with recommended extensions that can automate parts of the development process when installed. 11 | 12 | ## Troubleshooting 13 | 14 | The most common problem with building and developing in the repo is having the wrong version of Node.JS. Check your version with: 15 | 16 | ```bash 17 | node version 18 | ``` 19 | 20 | Another common problem is having `yarn` installed globally. This can override the version specified in our `package.json`. Check your version with: 21 | 22 | ```bash 23 | yarn --version 24 | ``` 25 | 26 | This should make the version specified in `package.json` in the `packageManager` field. 27 | 28 | ## Commom commands 29 | 30 | You will need these commands to successfully develop the Dashboard and contribute core to the repository. 31 | 32 | **Install dependencies:** 33 | 34 | ```bash 35 | yarn install 36 | ``` 37 | 38 | --- 39 | 40 | **Running the Dashboard locally:** 41 | 42 | _Run in separate terminals:_ 43 | 44 | ```bash 45 | kubectl proxy 46 | ``` 47 | 48 | ```bash 49 | yarn dev 50 | ``` 51 | 52 | You can leave these commands running while you work on the Dashboard. Changes you make will be reflected in the browser pretty immediately. 53 | 54 | The Dashboard is configured to look for a Kubernetes API server on `localhost:8001`. Running `kubectl proxy` will open a port-forward to your currently-configured Kubernetes cluster. 55 | 56 | This will launch the Dashboard at `http://localhost:3000`. 57 | 58 | --- 59 | 60 | **Compile TypeScript:** 61 | 62 | ```bash 63 | yarn run tsc 64 | ``` 65 | 66 | If you make changes to multiple packages at once, you may need to may need to manually run the TypeScript compiler. 67 | 68 | --- 69 | 70 | **Run tests:** 71 | 72 | ```bash 73 | yarn run test:all 74 | ``` 75 | 76 | If you have the Jest extension installed in VS Code the tests will run every time you make a file change. 77 | 78 | --- 79 | 80 | This project is configured to use `prettier` for code formatting. This is checked as part of the pull-request process. 81 | 82 | **Run the formatter to automatically fix violations:** 83 | 84 | ```bash 85 | yarn run format:write 86 | ``` 87 | 88 | --- 89 | 90 | This project is configured to use `eslint` for linting, along with recommended rules for React and TypeScript. This is checked as part of the pull-request process. 91 | 92 | **Run the linter:** 93 | 94 | ```bash 95 | yarn run lint:all 96 | ``` 97 | 98 | ## Complete command reference 99 | 100 | This is a more complete command reference for what our packages support. You will likely only need these commands in specialized cases. 101 | 102 | ### Building the code 103 | 104 | **Build all packages:** 105 | 106 | ```bash 107 | yarn workspaces foreach -A run build:all 108 | ``` 109 | 110 | **Build a specific package** 111 | 112 | ```bash 113 | # Substitute rad-components with any package name 114 | yarn workspace @radapp.io/rad-components run build 115 | ``` 116 | 117 | **Build the dashboard container** 118 | 119 | ```bash 120 | yarn install 121 | yarn tsc 122 | yarn build:backend --config ../../app-config.yaml --config ../../app-config.dashboard.yaml 123 | yarn build-image 124 | ``` 125 | 126 | The current `Dockerfile` is set up to build the container using state from the repo like the `node_modules/` folder. This is why the set of steps described above is required. 127 | 128 | ### Formatting 129 | 130 | This project is configured to use `prettier` for code formatting. This is checked as part of the pull-request process. 131 | 132 | **Run the formatter to find violations:** 133 | 134 | ```bash 135 | yarn run format:check 136 | ``` 137 | 138 | **Run the formatter to automatically fix violations:** 139 | 140 | ```bash 141 | yarn run format:write 142 | ``` 143 | 144 | ### Linting 145 | 146 | This project is configured to use `eslint` for linting, along with recommended rules for React and TypeScript. This is checked as part of the pull-request process. 147 | 148 | **Run the linter manually:** 149 | 150 | ```bash 151 | yarn run lint:all 152 | ``` 153 | 154 | ### Testing 155 | 156 | **Run E2E tests:** 157 | 158 | ```bash 159 | yarn run test:e2e 160 | ``` 161 | 162 | ### Scripts 163 | 164 | **Run a specific script in a specific package** 165 | 166 | ```bash 167 | # Substitute rad-components with any package name 168 | # Substitute link with any script name 169 | yarn workspace @radapp.io/rad-components run lint 170 | ``` 171 | -------------------------------------------------------------------------------- /docs/contributing/contributing-code/contributing-code-developing/README.md: -------------------------------------------------------------------------------- 1 | # Developing components of the Radius Dashboard 2 | 3 | This page how to run and develop the Radius Dashboard and its components. 4 | 5 | ## Developing: Dashboard 6 | 7 | **Launch the Dashboard:** 8 | 9 | _Run in separate terminals:_ 10 | 11 | ```bash 12 | kubectl proxy 13 | ``` 14 | 15 | ```bash 16 | yarn dev 17 | ``` 18 | 19 | You can leave these commands running while you work on the Dashboard. Changes you make will be reflected in the browser pretty immediately. 20 | 21 | The Dashboard is configured to look for a Kubernetes API server on `localhost:8001`. Running `kubectl proxy` will open a port-forward to your currently-configured Kubernetes cluster. 22 | 23 | This will launch the Dashboard at `http://localhost:3000`. 24 | 25 | ### Configuration 26 | 27 | The configuration for local development (`yarn dev`) is stored in `app-config.local.yaml`. This file is a set of overrides for development that will be combined with `app-config.yaml`. See the configuration file comments for links to relevant documentation. 28 | 29 | This file is checked in but `.gitignored`'d. Feel free to make changes as needed. 30 | 31 | The `app-config.dashboard.yaml` configuration is used when deployed as part of a Radius installation. 32 | 33 | ## Developing: rad-components 34 | 35 | **Launch Storybook to experiment with rad-components:** 36 | 37 | ```bash 38 | yarn workspace @radapp.io/rad-components run storybook 39 | ``` 40 | 41 | This will launch Storybook at `http://localhost:6006`. 42 | -------------------------------------------------------------------------------- /docs/contributing/contributing-code/contributing-code-organization/README.md: -------------------------------------------------------------------------------- 1 | # Understanding the Radius Dashboard repo code organization 2 | 3 | This repo uses [corepack](https://nodejs.org/api/corepack.html) and [yarn workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/). 4 | 5 | It is organized as a mono-repo, and contains the following packages. 6 | 7 | | Package | Path | Description | 8 | | ---------------------------------- | --------------------------------- | ------------------------------------------------------------------------------- | 9 | | `<root>` | `.` | The root `package.json` for the repo. Used as the entrypoint for most commands. | 10 | | `@internal/app` | `./packages/app` | The frontend (React) part of the Dashboard. | 11 | | `@internal/backend` | `./packages/backend` | The backend (Node.js) part of the Dashboard. | 12 | | `@radapp.io/rad-components` | `./packages/rad-components` | A library of reusable React components for Radius (no dependency on Backstage). | 13 | | `@radapp.io/plugin-radius` | `./plugins/plugin-radius` | The Radius frontend (React) plugin for Backstage. | 14 | | `@radapp.io/plugin-radius-backend` | `./plugins/plugin-radius-backend` | The Radius backend (Node.js) plugin for Backstage. | 15 | 16 | ### Understanding the organization 17 | 18 | Our repo builds three primary outputs: 19 | 20 | - The Radius Dashboard: this is a skinned deployment of Backstage with the layout, and set of plugins optimized for Radius. 21 | - The Radius Backstage plugin: our plugin is available standalone (outside of the Dashboard) so users can add our functionality to Backstage. 22 | - The `@radapp.io/rad-components` library: contains reusable UI like the Radius App Graph visualization. This is a separate package so we can use it in other contexts besides Backstage. 23 | 24 | The Radius Backstage plugin and `@radapp.io/rad-components` are published to NPM under the `@radapp.io` organization. The `@internal` organization prefix is used with our other packages to avoid conflicts with other public NPM packages. 25 | -------------------------------------------------------------------------------- /docs/contributing/contributing-issues/README.md: -------------------------------------------------------------------------------- 1 | # Contributing Issues 2 | 3 | You can open an issue using the form [here](https://github.com/radius-project/dashboard/issues/new/choose). This form will ask you to fill out a template based on the kind of issue you choose. Please fill out the form as this will help us respond to your issue. 4 | 5 | ## Tips for creating good issues 6 | 7 | ### Use the correct template 8 | 9 | You will save us (the maintainers) time if you want to use the right template. 10 | 11 | - Choose 'Bug Report' if some functionality in Radius is broken, crashing, or not working as advertised. 12 | - Choose 'Feature Request' if you have new ideas for us, or think some existing functionality should work differently. 13 | - Choose 'Engineering' if you have suggestions for improving the engineering system, build, ci/cd pipelines or processes. 14 | - Choose 'Open a blank issue' (at the bottom) if neither of those is a good fit. 15 | 16 | ### Focus on the repro steps 17 | 18 | Providing clear repro steps with code samples is the best way to get a good response to your issue. 19 | 20 | Remember that another human will need to read your instructions and try to reproduce your steps to understand the issue. 21 | 22 | ### Tell us what you tried 23 | 24 | If you tried to troubleshoot or workaround the problem please tell us what you tried. This will often save a lot of time in bug investigation and might help others that are working through the same issue. 25 | 26 | ### Include screenshots 27 | 28 | You can paste screenshots directly into a Github issue! 29 | -------------------------------------------------------------------------------- /examples/entities.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-system 3 | apiVersion: backstage.io/v1alpha1 4 | kind: System 5 | metadata: 6 | name: examples 7 | spec: 8 | owner: guests 9 | --- 10 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component 11 | apiVersion: backstage.io/v1alpha1 12 | kind: Component 13 | metadata: 14 | name: example-website 15 | spec: 16 | type: website 17 | lifecycle: experimental 18 | owner: guests 19 | system: examples 20 | --- 21 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component 22 | apiVersion: backstage.io/v1alpha1 23 | kind: Component 24 | metadata: 25 | name: example-service 26 | annotations: 27 | backstage.io/kubernetes-id: example-service 28 | spec: 29 | type: service 30 | lifecycle: experimental 31 | owner: guests 32 | system: examples 33 | providesApis: [example-grpc-api] 34 | --- 35 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-api 36 | apiVersion: backstage.io/v1alpha1 37 | kind: API 38 | metadata: 39 | name: example-grpc-api 40 | spec: 41 | type: grpc 42 | lifecycle: experimental 43 | owner: guests 44 | system: examples 45 | definition: | 46 | syntax = "proto3"; 47 | 48 | service Exampler { 49 | rpc Example (ExampleMessage) returns (ExampleMessage) {}; 50 | } 51 | 52 | message ExampleMessage { 53 | string example = 1; 54 | }; 55 | -------------------------------------------------------------------------------- /examples/org.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-user 3 | apiVersion: backstage.io/v1alpha1 4 | kind: User 5 | metadata: 6 | name: guest 7 | spec: 8 | memberOf: [guests] 9 | --- 10 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-group 11 | apiVersion: backstage.io/v1alpha1 12 | kind: Group 13 | metadata: 14 | name: guests 15 | spec: 16 | type: team 17 | children: [] 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "0.0.1", 4 | "repository": "git@github.com/radius-project/dashboard.git", 5 | "author": "The Radius Authors", 6 | "private": true, 7 | "packageManager": "yarn@4.0.2", 8 | "engines": { 9 | "node": "18 || 20 || 21" 10 | }, 11 | "scripts": { 12 | "dev": "concurrently \"yarn start\" \"yarn start-backend\"", 13 | "start": "yarn workspace @internal/app start", 14 | "start-backend": "yarn workspace @internal/backend start", 15 | "build:backend": "yarn workspace @internal/backend build", 16 | "build:all": "yarn tsc && backstage-cli repo build --all", 17 | "build-image": "yarn workspace @internal/backend build-image", 18 | "bundle:analyze": "yarn dlx source-map-explorer packages/app/dist/static/*.js", 19 | "tsc": "tsc", 20 | "tsc:full": "tsc --skipLibCheck false --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": "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 | "format:check": "prettier --check .", 29 | "format:write": "prettier --write .", 30 | "new": "backstage-cli new --scope internal" 31 | }, 32 | "workspaces": { 33 | "packages": [ 34 | "packages/*", 35 | "plugins/*" 36 | ] 37 | }, 38 | "devDependencies": { 39 | "@backstage/cli": "^0.25.0", 40 | "@backstage/e2e-test-utils": "^0.1.0", 41 | "@playwright/test": "^1.32.3", 42 | "@spotify/prettier-config": "^12.0.0", 43 | "@typescript-eslint/utils": "^6.16.0", 44 | "concurrently": "^8.0.0", 45 | "eslint": "^8.56.0", 46 | "node-gyp": "^9.0.0", 47 | "prettier": "^2.3.2", 48 | "typescript": "~5.2.0" 49 | }, 50 | "resolutions": { 51 | "@types/react": "^17", 52 | "@types/react-dom": "^17", 53 | "@backstage/backend-common": "^0.20.0", 54 | "jsonpath-plus": "^10", 55 | "mysql2": "^3" 56 | }, 57 | "prettier": "@spotify/prettier-config", 58 | "lint-staged": { 59 | "*.{js,jsx,ts,tsx,mjs,cjs}": [ 60 | "eslint --fix", 61 | "prettier --write" 62 | ], 63 | "*.{json,md}": [ 64 | "prettier --write" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/app/.eslintignore: -------------------------------------------------------------------------------- 1 | public 2 | -------------------------------------------------------------------------------- /packages/app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 2 | -------------------------------------------------------------------------------- /packages/app/e2e-tests/app.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 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 { test, expect } from '@playwright/test'; 18 | 19 | test('App should render the home page', async ({ page }) => { 20 | await page.goto('/'); 21 | 22 | await expect(page.getByText('Learn More')).toBeVisible(); 23 | await expect(page.getByText('Join the Community')).toBeVisible(); 24 | await expect(page.getByText('Get help with Radius')).toBeVisible(); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/app", 3 | "version": "0.0.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.6", 18 | "@backstage/catalog-model": "^1.4.3", 19 | "@backstage/cli": "^0.25.0", 20 | "@backstage/core-app-api": "^1.11.2", 21 | "@backstage/core-components": "^0.13.9", 22 | "@backstage/core-plugin-api": "^1.8.1", 23 | "@backstage/integration-react": "^1.1.22", 24 | "@backstage/plugin-catalog": "^1.16.0", 25 | "@backstage/plugin-catalog-common": "^1.0.19", 26 | "@backstage/plugin-catalog-graph": "^0.3.2", 27 | "@backstage/plugin-home": "^0.6.0", 28 | "@backstage/plugin-kubernetes": "^0.11.3", 29 | "@backstage/plugin-permission-react": "^0.4.18", 30 | "@backstage/plugin-user-settings": "^0.7.14", 31 | "@backstage/theme": "^0.5.0", 32 | "@internal/plugin-radius": "^0.1.0", 33 | "@material-ui/core": "^4.12.2", 34 | "@material-ui/icons": "^4.9.1", 35 | "history": "^5.0.0", 36 | "jest-canvas-mock": "^2.5.2", 37 | "react": "^17.0.2", 38 | "react-dom": "^17.0.2", 39 | "react-router": "^7.5.2", 40 | "react-router-dom": "^6.3.0", 41 | "react-use": "^17.2.4" 42 | }, 43 | "devDependencies": { 44 | "@backstage/test-utils": "^1.4.6", 45 | "@playwright/test": "^1.32.3", 46 | "@testing-library/dom": "^8.0.0", 47 | "@testing-library/jest-dom": "^5.10.1", 48 | "@testing-library/react": "^12.1.3", 49 | "@testing-library/user-event": "^14.0.0", 50 | "@types/react": "^17", 51 | "@types/react-dom": "^17", 52 | "cross-env": "^7.0.0" 53 | }, 54 | "browserslist": { 55 | "production": [ 56 | ">0.2%", 57 | "not dead", 58 | "not op_mini all" 59 | ], 60 | "development": [ 61 | "last 1 chrome version", 62 | "last 1 firefox version", 63 | "last 1 safari version" 64 | ] 65 | }, 66 | "files": [ 67 | "dist" 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /packages/app/public/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/android-icon-144x144.png -------------------------------------------------------------------------------- /packages/app/public/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/android-icon-192x192.png -------------------------------------------------------------------------------- /packages/app/public/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/android-icon-36x36.png -------------------------------------------------------------------------------- /packages/app/public/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/android-icon-48x48.png -------------------------------------------------------------------------------- /packages/app/public/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/android-icon-72x72.png -------------------------------------------------------------------------------- /packages/app/public/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/android-icon-96x96.png -------------------------------------------------------------------------------- /packages/app/public/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/apple-icon-114x114.png -------------------------------------------------------------------------------- /packages/app/public/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/apple-icon-120x120.png -------------------------------------------------------------------------------- /packages/app/public/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/apple-icon-144x144.png -------------------------------------------------------------------------------- /packages/app/public/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/apple-icon-152x152.png -------------------------------------------------------------------------------- /packages/app/public/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/apple-icon-180x180.png -------------------------------------------------------------------------------- /packages/app/public/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/apple-icon-57x57.png -------------------------------------------------------------------------------- /packages/app/public/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/apple-icon-60x60.png -------------------------------------------------------------------------------- /packages/app/public/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/apple-icon-72x72.png -------------------------------------------------------------------------------- /packages/app/public/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/apple-icon-76x76.png -------------------------------------------------------------------------------- /packages/app/public/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/apple-icon-precomposed.png -------------------------------------------------------------------------------- /packages/app/public/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/apple-icon.png -------------------------------------------------------------------------------- /packages/app/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig> -------------------------------------------------------------------------------- /packages/app/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/favicon-16x16.png -------------------------------------------------------------------------------- /packages/app/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/favicon-32x32.png -------------------------------------------------------------------------------- /packages/app/public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/favicon-96x96.png -------------------------------------------------------------------------------- /packages/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/favicon.ico -------------------------------------------------------------------------------- /packages/app/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 | <meta name="theme-color" content="#000000" /> 7 | <meta 8 | name="description" 9 | content="Radius dashboard provides insight into deployed applications and environments" 10 | /> 11 | <!-- 12 | manifest.json provides metadata used when your web app is installed on a 13 | user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ 14 | --> 15 | <link 16 | rel="manifest" 17 | href="<%= publicPath %>/manifest.json" 18 | crossorigin="use-credentials" 19 | /> 20 | <link rel="icon" href="<%= publicPath %>/favicon.ico" /> 21 | <link rel="shortcut icon" href="<%= publicPath %>/favicon.ico" /> 22 | <link 23 | rel="icon" 24 | type="image/png" 25 | sizes="16x16" 26 | href="<%= publicPath %>/favicon-96x96.png" 27 | /> 28 | <link 29 | rel="icon" 30 | type="image/png" 31 | sizes="32x32" 32 | href="<%= publicPath %>/favicon-32x32.png" 33 | /> 34 | <link 35 | rel="icon" 36 | type="image/png" 37 | sizes="16x16" 38 | href="<%= publicPath %>/favicon-16x16.png" 39 | /> 40 | <title><%= config.getString('app.title') %> 41 | 42 | 43 | 44 |
45 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /packages/app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Radius Dashboard", 3 | "name": "Radius Dashboard", 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/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/ms-icon-144x144.png -------------------------------------------------------------------------------- /packages/app/public/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/ms-icon-150x150.png -------------------------------------------------------------------------------- /packages/app/public/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/ms-icon-310x310.png -------------------------------------------------------------------------------- /packages/app/public/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radius-project/dashboard/4a23d5b4ee09d177d3a14f5bc4780c8181f11aca/packages/app/public/ms-icon-70x70.png -------------------------------------------------------------------------------- /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 | import React from 'react'; 2 | import { renderWithEffects } from '@backstage/test-utils'; 3 | import App from './App'; 4 | 5 | describe('App', () => { 6 | it('should render', async () => { 7 | process.env = { 8 | NODE_ENV: 'test', 9 | APP_CONFIG: [ 10 | { 11 | data: { 12 | app: { title: 'Test' }, 13 | backend: { baseUrl: 'http://localhost:7007' }, 14 | techdocs: { 15 | storageUrl: 'http://localhost:7007/api/techdocs/static/docs', 16 | }, 17 | }, 18 | context: 'test', 19 | }, 20 | ] as unknown as string, 21 | }; 22 | 23 | const rendered = await renderWithEffects(); 24 | expect(rendered.baseElement).toBeInTheDocument(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | import { catalogPlugin } from '@backstage/plugin-catalog'; 4 | import { UserSettingsPage } from '@backstage/plugin-user-settings'; 5 | import { apis } from './apis'; 6 | import { HomepageCompositionRoot } from '@backstage/plugin-home'; 7 | import { Root } from './components/Root'; 8 | 9 | import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; 10 | import { createApp } from '@backstage/app-defaults'; 11 | import { AppRouter, FlatRoutes } from '@backstage/core-app-api'; 12 | import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; 13 | import { 14 | ApplicationListPage, 15 | EnvironmentListPage, 16 | RecipeListPage, 17 | ResourceListPage, 18 | ResourcePage, 19 | radiusPlugin, 20 | } from '@internal/plugin-radius'; 21 | import { kubernetesPlugin } from '@backstage/plugin-kubernetes'; 22 | import { 23 | UnifiedThemeProvider, 24 | createBaseThemeOptions, 25 | createUnifiedTheme, 26 | genPageTheme, 27 | palettes, 28 | shapes, 29 | } from '@backstage/theme'; 30 | import { HomePage } from './components/home/HomePage'; 31 | 32 | const lightTheme = createUnifiedTheme({ 33 | ...createBaseThemeOptions({ 34 | palette: { 35 | ...palettes.light, 36 | primary: { 37 | main: '#db4c24', 38 | }, 39 | }, 40 | }), 41 | defaultPageTheme: 'other', 42 | pageTheme: { 43 | other: genPageTheme({ colors: ['#db4c24', '#db4c24'], shape: shapes.wave }), 44 | }, 45 | }); 46 | 47 | const darkTheme = createUnifiedTheme({ 48 | ...createBaseThemeOptions({ 49 | // https://m2.material.io/inline-tools/color/ 50 | palette: { 51 | ...palettes.dark, 52 | primary: { 53 | main: '#db4c24', 54 | }, 55 | }, 56 | }), 57 | defaultPageTheme: 'other', 58 | pageTheme: { 59 | other: genPageTheme({ colors: ['#db4c24', '#db4c24'], shape: shapes.wave }), 60 | }, 61 | }); 62 | 63 | const app = createApp({ 64 | apis, 65 | themes: [ 66 | { 67 | id: 'light', 68 | title: 'Light', 69 | variant: 'light', 70 | Provider: ({ children }) => ( 71 | 72 | ), 73 | }, 74 | { 75 | id: 'dark', 76 | title: 'Dark', 77 | variant: 'dark', 78 | Provider: ({ children }) => ( 79 | 80 | ), 81 | }, 82 | ], 83 | plugins: [ 84 | // Called for side-effect since we're not using their UI. 85 | kubernetesPlugin, 86 | ], 87 | bindRoutes({ bind }) { 88 | bind(radiusPlugin.externalRoutes, {}); 89 | bind(catalogPlugin.externalRoutes, {}); 90 | }, 91 | }); 92 | 93 | const routes = ( 94 | 95 | }> 96 | 97 | 98 | } /> 99 | } /> 100 | } /> 101 | } /> 102 | } /> 103 | } /> 104 | } 107 | /> 108 | 109 | ); 110 | 111 | export default app.createRoot( 112 | <> 113 | 114 | 115 | 116 | {routes} 117 | 118 | , 119 | ); 120 | -------------------------------------------------------------------------------- /packages/app/src/apis.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ScmIntegrationsApi, 3 | scmIntegrationsApiRef, 4 | ScmAuth, 5 | } from '@backstage/integration-react'; 6 | import { 7 | AnyApiFactory, 8 | configApiRef, 9 | createApiFactory, 10 | } from '@backstage/core-plugin-api'; 11 | 12 | export const apis: AnyApiFactory[] = [ 13 | createApiFactory({ 14 | api: scmIntegrationsApiRef, 15 | deps: { configApi: configApiRef }, 16 | factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), 17 | }), 18 | ScmAuth.createDefaultApiFactory(), 19 | ]; 20 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/Root.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from 'react'; 2 | import { makeStyles } from '@material-ui/core'; 3 | import { 4 | Settings as SidebarSettings, 5 | UserSettingsSignInAvatar, 6 | } from '@backstage/plugin-user-settings'; 7 | import { 8 | Sidebar, 9 | sidebarConfig, 10 | SidebarDivider, 11 | SidebarGroup, 12 | SidebarItem, 13 | SidebarPage, 14 | SidebarScrollWrapper, 15 | SidebarSpace, 16 | useSidebarOpenState, 17 | Link, 18 | } from '@backstage/core-components'; 19 | import MenuIcon from '@material-ui/icons/Menu'; 20 | import { 21 | RadiusLogo, 22 | RadiusLogomarkReverse, 23 | ApplicationIcon, 24 | EnvironmentIcon, 25 | ResourceIcon, 26 | RecipeIcon, 27 | } from '@internal/plugin-radius'; 28 | 29 | const useSidebarLogoStyles = makeStyles({ 30 | root: { 31 | width: sidebarConfig.drawerWidthClosed, 32 | height: 3 * sidebarConfig.logoHeight, 33 | display: 'flex', 34 | flexFlow: 'row nowrap', 35 | alignItems: 'center', 36 | marginBottom: -14, 37 | }, 38 | link: { 39 | width: sidebarConfig.drawerWidthClosed, 40 | marginLeft: 24, 41 | }, 42 | svg: { 43 | width: 'auto', 44 | height: 28, 45 | }, 46 | }); 47 | 48 | const SidebarLogo = () => { 49 | const classes = useSidebarLogoStyles(); 50 | const { isOpen } = useSidebarOpenState(); 51 | 52 | return ( 53 |
54 | 55 | {isOpen ? ( 56 | 57 | ) : ( 58 | 59 | )} 60 | 61 |
62 | ); 63 | }; 64 | 65 | export const Root = ({ children }: PropsWithChildren>) => ( 66 | 67 | 68 | 69 | 70 | }> 71 | {/* Global nav, not org-specific */} 72 | 73 | 78 | 83 | 84 | 85 | {/* End global nav */} 86 | 87 | 88 | 89 | 90 | 91 | } 94 | to="/settings" 95 | > 96 | 97 | 98 | 99 | {children} 100 | 101 | ); 102 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/index.ts: -------------------------------------------------------------------------------- 1 | export { Root } from './Root'; 2 | -------------------------------------------------------------------------------- /packages/app/src/components/home/CommunityCard.tsx: -------------------------------------------------------------------------------- 1 | import { InfoCard, LinkButton } from '@backstage/core-components'; 2 | import { CardActions, Typography } from '@material-ui/core'; 3 | 4 | import React from 'react'; 5 | 6 | const actions = () => ( 7 | 8 | 13 | Visit on Github 14 | 15 | 20 | Good first issues 21 | 22 | 23 | Join us on Discord 24 | 25 | 26 | ); 27 | 28 | export const CommunityCard = ({ className }: { className?: string }) => ( 29 | 35 | 36 | We welcome and encourage users to contribute to the Radius open-source 37 | project in various ways. By joining our community, you can make a 38 | meaningful impact and help shape the future of this project. 39 | 40 | 41 | ); 42 | 43 | export default CommunityCard; 44 | -------------------------------------------------------------------------------- /packages/app/src/components/home/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import { HomePageCompanyLogo } from '@backstage/plugin-home'; 2 | import { Grid, makeStyles } from '@material-ui/core'; 3 | import React from 'react'; 4 | import { Content, Page } from '@backstage/core-components'; 5 | import { 6 | ApplicationListInfoCard, 7 | EnvironmentListInfoCard, 8 | RadiusLogo, 9 | } from '@internal/plugin-radius'; 10 | import LearnCard from './LearnCard'; 11 | import CommunityCard from './CommunityCard'; 12 | import SupportCard from './SupportCard'; 13 | 14 | const useStyles = makeStyles(theme => ({ 15 | container: { 16 | margin: theme.spacing(5, 0), 17 | }, 18 | infoCard: { 19 | height: '100%', 20 | }, 21 | svg: { 22 | width: 'auto', 23 | height: 100, 24 | }, 25 | })); 26 | 27 | export const HomePage = () => { 28 | const { container, infoCard, svg } = useStyles(); 29 | 30 | return ( 31 | 32 | 33 | 34 | } 37 | /> 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /packages/app/src/components/home/LearnCard.tsx: -------------------------------------------------------------------------------- 1 | import { InfoCard, LinkButton } from '@backstage/core-components'; 2 | import { CardActions, Typography } from '@material-ui/core'; 3 | 4 | import React from 'react'; 5 | 6 | const actions = () => ( 7 | 8 | 13 | Get Started 14 | 15 | 20 | Tutorials 21 | 22 | 27 | Reference 28 | 29 | 30 | ); 31 | 32 | export const LearnCard = ({ className }: { className?: string }) => ( 33 | 39 | 40 | Radius is an open-source, cloud-native, application platform that enables 41 | developers and the operators that support them to define, deploy, and 42 | collaborate on cloud-native applications across public clouds and private 43 | infrastructure 44 | 45 | 46 | ); 47 | 48 | export default LearnCard; 49 | -------------------------------------------------------------------------------- /packages/app/src/components/home/SupportCard.tsx: -------------------------------------------------------------------------------- 1 | import { InfoCard, LinkButton } from '@backstage/core-components'; 2 | import { CardActions, Typography } from '@material-ui/core'; 3 | 4 | import React from 'react'; 5 | 6 | const actions = () => ( 7 | 8 | 13 | Ask a Question 14 | 15 | 20 | Report an Issue 21 | 22 | 23 | ); 24 | 25 | export const SupportCard = ({ className }: { className?: string }) => ( 26 | 32 | 33 | Participate in discussions, forums, and chat channels related to Radius. 34 | Seek guidance, offer help to others, and build connections within the 35 | community. 36 | 37 | 38 | ); 39 | 40 | export default SupportCard; 41 | -------------------------------------------------------------------------------- /packages/app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import '@backstage/cli/asset-types'; 2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom'; 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /packages/app/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import 'jest-canvas-mock'; 3 | -------------------------------------------------------------------------------- /packages/backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 2 | -------------------------------------------------------------------------------- /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 | # Ensure that we use the correct version of Yarn. 15 | RUN corepack enable && yarn -v 16 | 17 | # Install sqlite3 dependencies. You can skip this if you don't use sqlite3 in the image, 18 | # in which case you should also move better-sqlite3 to "devDependencies" in package.json. 19 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ 20 | --mount=type=cache,target=/var/lib/apt,sharing=locked \ 21 | apt-get update && \ 22 | apt-get install -y --no-install-recommends libsqlite3-dev 23 | 24 | # From here on we use the least-privileged `node` user to run the backend. 25 | USER node 26 | 27 | # This should create the app dir as `node`. 28 | # If it is instead created as `root` then the `tar` command below will fail: `can't create directory 'packages/': Permission denied`. 29 | # If this occurs, then ensure BuildKit is enabled (`DOCKER_BUILDKIT=1`) so the app dir is correctly created as `node`. 30 | WORKDIR /app 31 | 32 | # This switches many Node.js dependencies to production mode. 33 | ENV NODE_ENV production 34 | 35 | # Copy repo skeleton first, to avoid unnecessary docker cache invalidation. 36 | # The skeleton contains the package.json of each package in the monorepo, 37 | # and along with yarn.lock and the root package.json, that's enough to run yarn install. 38 | COPY --chown=node:node .yarnrc.yml yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ 39 | RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz 40 | 41 | RUN --mount=type=cache,target=/home/node/.yarn,sharing=locked,uid=1000,gid=1000 \ 42 | YARN_CACHE_FOLDER=/home/node/.yarn yarn workspaces focus --production @internal/backend 43 | 44 | # Then copy the rest of the backend bundle, along with any other files we might want. 45 | COPY --chown=node:node packages/backend/dist/bundle.tar.gz app-config*.yaml ./ 46 | RUN tar xzf bundle.tar.gz && rm bundle.tar.gz 47 | 48 | CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.dashboard.yaml"] 49 | -------------------------------------------------------------------------------- /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 | ``` 20 | 21 | You should only need to do this once. 22 | 23 | After that, go to the `packages/backend` directory and run 24 | 25 | ```bash 26 | yarn start 27 | ``` 28 | 29 | If you want to override any configuration locally, for example adding any secrets, 30 | you can do so in `app-config.local.yaml`. 31 | 32 | The backend starts up on port 7007 per default. 33 | 34 | ## Populating The Catalog 35 | 36 | If you want to use the catalog functionality, you need to add so called 37 | locations to the backend. These are places where the backend can find some 38 | entity descriptor data to consume and serve. For more information, see 39 | [Software Catalog Overview - Adding Components to the Catalog](https://backstage.io/docs/features/software-catalog/#adding-components-to-the-catalog). 40 | 41 | To get started quickly, this template already includes some statically configured example locations 42 | in `app-config.yaml` under `catalog.locations`. You can remove and replace these locations as you 43 | like, and also override them for local development in `app-config.local.yaml`. 44 | 45 | ## Authentication 46 | 47 | We chose [Passport](http://www.passportjs.org/) as authentication platform due 48 | to its comprehensive set of supported authentication 49 | [strategies](http://www.passportjs.org/packages/). 50 | 51 | Read more about the 52 | [auth-backend](https://github.com/backstage/backstage/blob/master/plugins/auth-backend/README.md) 53 | and 54 | [how to add a new provider](https://github.com/backstage/backstage/blob/master/docs/auth/add-auth-provider.md) 55 | 56 | ## Documentation 57 | 58 | - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) 59 | - [Backstage Documentation](https://backstage.io/docs) 60 | -------------------------------------------------------------------------------- /packages/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/backend", 3 | "version": "0.0.1", 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 -t ghcr.io/radius-project/dashboard" 17 | }, 18 | "dependencies": { 19 | "@backstage/backend-common": "^0.20.2", 20 | "@backstage/backend-tasks": "^0.5.13", 21 | "@backstage/catalog-client": "^1.5.1", 22 | "@backstage/catalog-model": "^1.4.3", 23 | "@backstage/config": "^1.1.1", 24 | "@backstage/plugin-app-backend": "^0.3.56", 25 | "@backstage/plugin-auth-backend": "^0.20.2", 26 | "@backstage/plugin-auth-node": "^0.4.2", 27 | "@backstage/plugin-catalog-backend": "^1.16.0", 28 | "@backstage/plugin-kubernetes-backend": "^0.14.0", 29 | "@backstage/plugin-kubernetes-node": "^0.1.2", 30 | "@backstage/plugin-permission-common": "^0.7.11", 31 | "@backstage/plugin-permission-node": "^0.7.19", 32 | "@backstage/plugin-proxy-backend": "^0.4.6", 33 | "@internal/app": "^0.0.1", 34 | "@internal/plugin-radius-backend": "^0.1.0", 35 | "better-sqlite3": "^9.0.0", 36 | "dockerode": "^3.3.1", 37 | "express": "^4.19.2", 38 | "express-promise-router": "^4.1.0", 39 | "node-gyp": "^9.0.0", 40 | "pg": "^8.11.3", 41 | "winston": "^3.2.1" 42 | }, 43 | "devDependencies": { 44 | "@backstage/cli": "^0.25.0", 45 | "@types/dockerode": "^3.3.0", 46 | "@types/express": "^4.17.6", 47 | "@types/express-serve-static-core": "^4.17.5", 48 | "@types/luxon": "^2.0.4" 49 | }, 50 | "files": [ 51 | "dist" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /packages/backend/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { PluginEnvironment } from './types'; 2 | 3 | describe('test', () => { 4 | it('unbreaks the test runner', () => { 5 | const unbreaker = {} as PluginEnvironment; 6 | expect(unbreaker).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /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 | import Router from 'express-promise-router'; 10 | import { 11 | createServiceBuilder, 12 | loadBackendConfig, 13 | getRootLogger, 14 | useHotMemoize, 15 | notFoundHandler, 16 | CacheManager, 17 | DatabaseManager, 18 | HostDiscovery, 19 | UrlReaders, 20 | ServerTokenManager, 21 | } from '@backstage/backend-common'; 22 | import { TaskScheduler } from '@backstage/backend-tasks'; 23 | import { Config } from '@backstage/config'; 24 | import app from './plugins/app'; 25 | import auth from './plugins/auth'; 26 | import catalog from './plugins/catalog'; 27 | import proxy from './plugins/proxy'; 28 | import { PluginEnvironment } from './types'; 29 | import { ServerPermissionClient } from '@backstage/plugin-permission-node'; 30 | import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; 31 | import kubernetes from './plugins/kubernetes'; 32 | 33 | function makeCreateEnv(config: Config) { 34 | const root = getRootLogger(); 35 | const reader = UrlReaders.default({ logger: root, config }); 36 | const discovery = HostDiscovery.fromConfig(config); 37 | const cacheManager = CacheManager.fromConfig(config); 38 | const databaseManager = DatabaseManager.fromConfig(config, { logger: root }); 39 | const tokenManager = ServerTokenManager.noop(); 40 | const taskScheduler = TaskScheduler.fromConfig(config, { databaseManager }); 41 | 42 | const identity = DefaultIdentityClient.create({ 43 | discovery, 44 | }); 45 | const permissions = ServerPermissionClient.fromConfig(config, { 46 | discovery, 47 | tokenManager, 48 | }); 49 | 50 | root.info(`Created UrlReader ${reader}`); 51 | 52 | return (plugin: string): PluginEnvironment => { 53 | const logger = root.child({ type: 'plugin', plugin }); 54 | const database = databaseManager.forPlugin(plugin); 55 | const cache = cacheManager.forPlugin(plugin); 56 | const scheduler = taskScheduler.forPlugin(plugin); 57 | return { 58 | logger, 59 | database, 60 | cache, 61 | config, 62 | reader, 63 | discovery, 64 | tokenManager, 65 | scheduler, 66 | permissions, 67 | identity, 68 | }; 69 | }; 70 | } 71 | 72 | async function main() { 73 | const config = await loadBackendConfig({ 74 | argv: process.argv, 75 | logger: getRootLogger(), 76 | }); 77 | const createEnv = makeCreateEnv(config); 78 | 79 | const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); 80 | const authEnv = useHotMemoize(module, () => createEnv('auth')); 81 | const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); 82 | const appEnv = useHotMemoize(module, () => createEnv('app')); 83 | const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes')); 84 | 85 | const apiRouter = Router(); 86 | apiRouter.use('/catalog', await catalog(catalogEnv)); 87 | apiRouter.use('/auth', await auth(authEnv)); 88 | apiRouter.use('/proxy', await proxy(proxyEnv)); 89 | apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv)); 90 | 91 | // Add backends ABOVE this line; this 404 handler is the catch-all fallback 92 | apiRouter.use(notFoundHandler()); 93 | 94 | const service = createServiceBuilder(module) 95 | .loadConfig(config) 96 | .addRouter('/api', apiRouter) 97 | .addRouter('', await app(appEnv)); 98 | 99 | await service.start().catch(err => { 100 | console.log(err); 101 | process.exit(1); 102 | }); 103 | } 104 | 105 | module.hot?.accept(); 106 | main().catch(error => { 107 | console.error('Backend failed to start up', error); 108 | process.exit(1); 109 | }); 110 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/app.ts: -------------------------------------------------------------------------------- 1 | import { createRouter } from '@backstage/plugin-app-backend'; 2 | import { Router } from 'express'; 3 | import { PluginEnvironment } from '../types'; 4 | 5 | export default async function createPlugin( 6 | env: PluginEnvironment, 7 | ): Promise { 8 | return await createRouter({ 9 | logger: env.logger, 10 | config: env.config, 11 | database: env.database, 12 | appPackageName: '@internal/app', 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/auth.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createRouter, 3 | providers, 4 | defaultAuthProviderFactories, 5 | } from '@backstage/plugin-auth-backend'; 6 | import { Router } from 'express'; 7 | import { PluginEnvironment } from '../types'; 8 | 9 | export default async function createPlugin( 10 | env: PluginEnvironment, 11 | ): Promise { 12 | return await createRouter({ 13 | logger: env.logger, 14 | config: env.config, 15 | database: env.database, 16 | discovery: env.discovery, 17 | tokenManager: env.tokenManager, 18 | providerFactories: { 19 | ...defaultAuthProviderFactories, 20 | 21 | // This replaces the default GitHub auth provider with a customized one. 22 | // The `signIn` option enables sign-in for this provider, using the 23 | // identity resolution logic that's provided in the `resolver` callback. 24 | // 25 | // This particular resolver makes all users share a single "guest" identity. 26 | // It should only be used for testing and trying out Backstage. 27 | // 28 | // If you want to use a production ready resolver you can switch to 29 | // the one that is commented out below, it looks up a user entity in the 30 | // catalog using the GitHub username of the authenticated user. 31 | // That resolver requires you to have user entities populated in the catalog, 32 | // for example using https://backstage.io/docs/integrations/github/org 33 | // 34 | // There are other resolvers to choose from, and you can also create 35 | // your own, see the auth documentation for more details: 36 | // 37 | // https://backstage.io/docs/auth/identity-resolver 38 | github: providers.github.create({ 39 | signIn: { 40 | resolver(_, ctx) { 41 | const userRef = 'user:default/guest'; // Must be a full entity reference 42 | return ctx.issueToken({ 43 | claims: { 44 | sub: userRef, // The user's own identity 45 | ent: [userRef], // A list of identities that the user claims ownership through 46 | }, 47 | }); 48 | }, 49 | // resolver: providers.github.resolvers.usernameMatchingUserEntityName(), 50 | }, 51 | }), 52 | }, 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/catalog.ts: -------------------------------------------------------------------------------- 1 | import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; 2 | import { Router } from 'express'; 3 | import { PluginEnvironment } from '../types'; 4 | 5 | export default async function createPlugin( 6 | env: PluginEnvironment, 7 | ): Promise { 8 | const builder = await CatalogBuilder.create(env); 9 | const { processingEngine, router } = await builder.build(); 10 | await processingEngine.start(); 11 | return router; 12 | } 13 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/kubernetes.ts: -------------------------------------------------------------------------------- 1 | import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend'; 2 | import { Router } from 'express'; 3 | import { PluginEnvironment } from '../types'; 4 | import { CatalogClient } from '@backstage/catalog-client'; 5 | 6 | export default async function createPlugin( 7 | env: PluginEnvironment, 8 | ): Promise { 9 | const catalogApi = new CatalogClient({ discoveryApi: env.discovery }); 10 | const { router } = await KubernetesBuilder.createBuilder({ 11 | logger: env.logger, 12 | config: env.config, 13 | catalogApi, 14 | permissions: env.permissions, 15 | }).build(); 16 | 17 | return router; 18 | } 19 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/proxy.ts: -------------------------------------------------------------------------------- 1 | import { createRouter } from '@backstage/plugin-proxy-backend'; 2 | import { Router } from 'express'; 3 | import { PluginEnvironment } from '../types'; 4 | 5 | export default async function createPlugin( 6 | env: PluginEnvironment, 7 | ): Promise { 8 | return await createRouter({ 9 | logger: env.logger, 10 | config: env.config, 11 | discovery: env.discovery, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /packages/backend/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from 'winston'; 2 | import { Config } from '@backstage/config'; 3 | import { 4 | PluginCacheManager, 5 | PluginDatabaseManager, 6 | PluginEndpointDiscovery, 7 | TokenManager, 8 | UrlReader, 9 | } from '@backstage/backend-common'; 10 | import { PluginTaskScheduler } from '@backstage/backend-tasks'; 11 | import { PermissionEvaluator } from '@backstage/plugin-permission-common'; 12 | import { IdentityApi } from '@backstage/plugin-auth-node'; 13 | 14 | export type PluginEnvironment = { 15 | logger: Logger; 16 | database: PluginDatabaseManager; 17 | cache: PluginCacheManager; 18 | config: Config; 19 | reader: UrlReader; 20 | discovery: PluginEndpointDiscovery; 21 | tokenManager: TokenManager; 22 | scheduler: PluginTaskScheduler; 23 | permissions: PermissionEvaluator; 24 | identity: IdentityApi; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/rad-components/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-webpack5'; 2 | 3 | import { join, dirname } from 'path'; 4 | 5 | /** 6 | * This function is used to resolve the absolute path of a package. 7 | * It is needed in projects that use Yarn PnP or are set up within a monorepo. 8 | */ 9 | function getAbsolutePath(value: string): any { 10 | return dirname(require.resolve(join(value, 'package.json'))); 11 | } 12 | const config: StorybookConfig = { 13 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 14 | addons: [ 15 | getAbsolutePath('@storybook/addon-links'), 16 | getAbsolutePath('@storybook/addon-essentials'), 17 | getAbsolutePath('@storybook/addon-onboarding'), 18 | getAbsolutePath('@storybook/addon-interactions'), 19 | ], 20 | framework: { 21 | name: getAbsolutePath('@storybook/react-webpack5'), 22 | options: { 23 | builder: { 24 | useSWC: true, 25 | }, 26 | }, 27 | }, 28 | docs: { 29 | autodocs: 'tag', 30 | }, 31 | }; 32 | export default config; 33 | -------------------------------------------------------------------------------- /packages/rad-components/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react'; 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | actions: { argTypesRegex: '^on[A-Z].*' }, 6 | controls: { 7 | matchers: { 8 | color: /(background|color)$/i, 9 | date: /Date$/i, 10 | }, 11 | }, 12 | }, 13 | }; 14 | 15 | export default preview; 16 | -------------------------------------------------------------------------------- /packages/rad-components/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-typescript", 5 | ["@babel/preset-react", { "runtime": "automatic" }] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/rad-components/eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // Specify the environments where the code will run 3 | "env": { 4 | "jest": true, // Enable Jest for testing 5 | "browser": true // Enable browser environment 6 | }, 7 | 8 | // Stop ESLint from searching for configuration in parent folders 9 | "root": true, 10 | 11 | // Specify the parser for TypeScript (using @typescript-eslint/parser) 12 | "parser": "@typescript-eslint/parser", // Leverages TS ESTree to lint TypeScript 13 | 14 | // Add additional rules and configuration options 15 | "plugins": ["@typescript-eslint"], 16 | 17 | // Extend various ESLint configurations and plugins 18 | "extends": [ 19 | "eslint:recommended", // ESLint recommended rules 20 | "plugin:react/recommended", // React recommended rules 21 | "plugin:@typescript-eslint/recommended", // TypeScript recommended rules 22 | "plugin:@typescript-eslint/eslint-recommended", // ESLint overrides for TypeScript 23 | "prettier", // Prettier rules 24 | "plugin:prettier/recommended", // Prettier plugin integration 25 | "plugin:react-hooks/recommended", // Recommended rules for React hooks 26 | "plugin:storybook/recommended" // Recommended rules for Storybook 27 | ], 28 | "rules": { 29 | "react/react-in-jsx-scope": "off", 30 | } 31 | } -------------------------------------------------------------------------------- /packages/rad-components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /packages/rad-components/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "coveragePathIgnorePatterns": [ 3 | "/node_modules/", 4 | "/.storybook/", 5 | "/**/__docs__/*" 6 | ], 7 | "moduleNameMapper": { 8 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", 9 | "\\.(css|less)$": "identity-obj-proxy" 10 | }, 11 | "setupFilesAfterEnv": ["/src/setupTests.ts"], 12 | "testEnvironment": "jsdom" 13 | } 14 | -------------------------------------------------------------------------------- /packages/rad-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@radapp.io/rad-components", 3 | "version": "0.0.8", 4 | "description": "Components for use with Radius", 5 | "type": "module", 6 | "main": "dist/index.cjs.js", 7 | "module": "dist/index.esm.js", 8 | "types": "dist/index.d.ts", 9 | "backstage": { 10 | "role": "web-library" 11 | }, 12 | "files": [ 13 | "/dist" 14 | ], 15 | "scripts": { 16 | "build": "backstage-cli package build", 17 | "lint": "backstage-cli package lint", 18 | "test": "backstage-cli package test", 19 | "clean": "backstage-cli package clean", 20 | "storybook": "storybook dev -p 6006", 21 | "build-storybook": "storybook build" 22 | }, 23 | "keywords": [], 24 | "author": "", 25 | "license": "ISC", 26 | "devDependencies": { 27 | "@babel/core": "^7.23.0", 28 | "@babel/preset-env": "^7.23.6", 29 | "@babel/preset-react": "^7.23.3", 30 | "@juggle/resize-observer": "^3.4.0", 31 | "@storybook/addon-essentials": "^7.6.6", 32 | "@storybook/addon-interactions": "^7.6.6", 33 | "@storybook/addon-links": "^7.6.6", 34 | "@storybook/addon-onboarding": "^1.0.10", 35 | "@storybook/blocks": "^7.6.6", 36 | "@storybook/react": "^7.6.6", 37 | "@storybook/react-webpack5": "^7.6.6", 38 | "@storybook/test": "^7.6.6", 39 | "@testing-library/jest-dom": "^6.1.5", 40 | "@testing-library/react": "^12.1.3", 41 | "@types/babel__preset-env": "^7", 42 | "@types/dagre": "^0", 43 | "@types/identity-obj-proxy": "^3", 44 | "@types/react": "^18.2.45", 45 | "@types/react-test-renderer": "^18", 46 | "@typescript-eslint/eslint-plugin": "^6.14.0", 47 | "@typescript-eslint/parser": "^6.14.0", 48 | "babel-jest": "^29.7.0", 49 | "eslint": "^8.55.0", 50 | "eslint-config-prettier": "^9.1.0", 51 | "eslint-plugin-prettier": "^5.0.1", 52 | "eslint-plugin-react": "^7.33.2", 53 | "eslint-plugin-react-hooks": "^4.6.0", 54 | "eslint-plugin-storybook": "^0.6.15", 55 | "identity-obj-proxy": "^3.0.0", 56 | "jest": "^29.7.0", 57 | "jsdom": "^23.0.1", 58 | "prettier": "^3.1.1", 59 | "react": "^17.0.0", 60 | "react-dom": "^17.0.0", 61 | "react-test-renderer": "^17.0.0", 62 | "storybook": "^7.6.4", 63 | "typescript": "^5.3.3" 64 | }, 65 | "peerDependencies": { 66 | "react": "^16.13.1 || ^17.0.2 || ^18.2.0", 67 | "react-dom": "^16.13.1 || ^17.0.2 || ^18.2.0" 68 | }, 69 | "dependencies": { 70 | "@dagrejs/dagre": "^1.0.4", 71 | "dagre": "^0.8.5", 72 | "reactflow": "^11.10.1" 73 | }, 74 | "publishConfig": { 75 | "registry": "https://registry.yarnpkg.com", 76 | "access": "public" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/rad-components/src/components/appgraph/AppGraph.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { 3 | ReactFlow, 4 | Edge, 5 | Node, 6 | useReactFlow, 7 | useNodesState, 8 | useEdgesState, 9 | ReactFlowProvider, 10 | Controls, 11 | } from 'reactflow'; 12 | import Dagre, { Label } from '@dagrejs/dagre'; 13 | import { AppGraph as AppGraphData, Resource } from '../../graph'; 14 | import { ResourceNode } from '../resourcenode/index'; 15 | 16 | import 'reactflow/dist/style.css'; 17 | import { parseResourceId } from '../../resourceId'; 18 | 19 | const nodeTypes = { default: ResourceNode }; 20 | 21 | const LayoutFlow = (props: { graph: AppGraphData }) => { 22 | const initial = initialNodes(props.graph); 23 | const layoutedNodes = getLayoutedElements(initial.nodes, initial.edges, { 24 | direction: 'TB', 25 | }); 26 | 27 | const { fitView } = useReactFlow(); 28 | const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes.nodes); 29 | const [edges, setEdges, onEdgesChange] = useEdgesState(layoutedNodes.edges); 30 | 31 | useCallback( 32 | direction => { 33 | const layouted = getLayoutedElements(nodes, edges, { direction }); 34 | 35 | setNodes([...layouted.nodes]); 36 | setEdges([...layouted.edges]); 37 | 38 | window.requestAnimationFrame(() => { 39 | fitView(); 40 | }); 41 | }, 42 | [nodes, edges], 43 | ); 44 | 45 | // Notes on our usage of ReactFlow: 46 | // 47 | // - We're using an uncontrolled flow: https://reactflow.dev/learn/advanced-use/uncontrolled-flow 48 | // - We implemented a custom node type: https://reactflow.dev/learn/customization/custom-nodes 49 | // - We're using Dagre for layout: https://reactflow.dev/learn/layouting/layouting#dagre 50 | 51 | return ( 52 | 61 | 62 | 63 | ); 64 | }; 65 | 66 | export type AppGraphProps = { graph: AppGraphData }; 67 | 68 | function AppGraph(props: AppGraphProps) { 69 | return ( 70 |
71 | 72 | 73 | 74 |
75 | ); 76 | } 77 | 78 | function initialNodes(graph: AppGraphData): { 79 | nodes: Node[]; 80 | edges: Edge[]; 81 | } { 82 | const nodes: Node[] = []; 83 | const edges: Edge[] = []; 84 | 85 | // Very simple layout scheme here for nodes. 86 | const orderData: { [order: number]: number } = {}; 87 | 88 | for (const resource of graph.resources) { 89 | // The computed 'Order' is used to compute the 'y' coordinate for the initial layout. 90 | // This is computed based on the number of inbound connections (or whether it's a container). 91 | const order = 92 | resource.connections?.filter(c => c.direction === 'Inbound').length || 93 | resource.type === 'Applications.Core/containers' 94 | ? 0 95 | : 3; 96 | 97 | // The computed 'Rank' is used to compute the 'x' coordinate for the initial layout. 98 | // This is computed based on the number of resources at the same 'Order'. 99 | const rank = (orderData[order] = (orderData[order] || 0) + 1); 100 | 101 | // This provides the initial bias for the layout. Dagre will adjust this. 102 | 103 | nodes.push({ 104 | id: resource.id, 105 | position: { x: rank * 50, y: order * 50 }, 106 | height: 250, 107 | width: 175, 108 | data: resource, 109 | type: 'default', 110 | }); 111 | 112 | if (resource.connections) { 113 | for (const connection of resource.connections) { 114 | // We have a bug where the connections have the wrong direction. 115 | const parsedConnection = parseResourceId(connection.id); 116 | if (!parsedConnection) { 117 | continue; 118 | } 119 | 120 | if ( 121 | connection.direction === 'Inbound' && 122 | parsedConnection.type === 'Applications.Core/gateways' 123 | ) { 124 | connection.direction = 'Outbound'; 125 | } 126 | } 127 | 128 | for (const connection of resource.connections) { 129 | if (connection.direction === 'Inbound') { 130 | edges.push({ 131 | id: `${connection.id}-${resource.id}`, 132 | source: resource.id, 133 | target: connection.id, 134 | }); 135 | } else { 136 | edges.push({ 137 | id: `${resource.id}-${connection.id}`, 138 | source: connection.id, 139 | target: resource.id, 140 | }); 141 | } 142 | } 143 | } 144 | } 145 | 146 | return { nodes, edges }; 147 | } 148 | 149 | const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); 150 | 151 | function getLayoutedElements( 152 | nodes: Node[], 153 | edges: Edge[], 154 | options: { direction: string }, 155 | ) { 156 | g.setGraph({ rankdir: options.direction }); 157 | 158 | edges.forEach(edge => g.setEdge(edge.source, edge.target)); 159 | nodes.forEach(node => g.setNode(node.id, node as Label)); 160 | 161 | Dagre.layout(g); 162 | 163 | return { 164 | nodes: nodes.map(node => { 165 | const { x, y } = g.node(node.id); 166 | 167 | return { ...node, position: { x, y } }; 168 | }), 169 | edges, 170 | }; 171 | } 172 | 173 | export default AppGraph; 174 | -------------------------------------------------------------------------------- /packages/rad-components/src/components/appgraph/__docs__/AppGraph.mdx: -------------------------------------------------------------------------------- 1 | import { Canvas, Meta } from '@storybook/blocks'; 2 | import Example from './Example.tsx'; 3 | import * as AppGraph from './AppGraph.stories.tsx'; 4 | 5 | 6 | 7 | # AppGraph 8 | 9 | AppGraph displays the topology of different applications. 10 | 11 | #### Example: Demo Application 12 | 13 | 14 | 15 | #### Arguments 16 | 17 | - **resource** _`AppGraph`_ - The application graph to display. 18 | -------------------------------------------------------------------------------- /packages/rad-components/src/components/appgraph/__docs__/AppGraph.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import Example from './Example'; 3 | import * as sampledata from '../../../sampledata'; 4 | import { AppGraphProps } from '../AppGraph'; 5 | 6 | const meta: Meta = { 7 | title: 'AppGraph', 8 | component: Example, 9 | }; 10 | 11 | export default meta; 12 | type Story = StoryObj; 13 | 14 | export const Demo: Story = { 15 | args: { 16 | graph: sampledata.DemoApplication, 17 | } as AppGraphProps, 18 | }; 19 | -------------------------------------------------------------------------------- /packages/rad-components/src/components/appgraph/__docs__/Example.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import AppGraph, { AppGraphProps } from '../AppGraph'; 3 | import * as sampledata from '../../../sampledata'; 4 | 5 | const Example: FC = ({ graph = sampledata.DemoApplication }) => { 6 | return ( 7 |
16 | 17 |
18 | ); 19 | }; 20 | 21 | export default Example; 22 | -------------------------------------------------------------------------------- /packages/rad-components/src/components/appgraph/__test__/AppGraph.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '@testing-library/jest-dom'; 3 | import { render, screen } from '@testing-library/react'; 4 | import AppGraph from '../AppGraph'; 5 | import * as sampledata from '../../../sampledata'; 6 | 7 | describe('AppGraph component', () => { 8 | it('AppGraph should render correctly', () => { 9 | const application = sampledata.DemoApplication; 10 | render(); 11 | 12 | // For now we just test that the ReactFlow attribution is present. This means 13 | // that the UI rendered. 14 | const name = screen.getByRole('link', { name: 'React Flow attribution' }); 15 | expect(name).toBeInTheDocument(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/rad-components/src/components/appgraph/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AppGraph } from './AppGraph'; 2 | -------------------------------------------------------------------------------- /packages/rad-components/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './appgraph'; 2 | export * from './resourcenode'; 3 | -------------------------------------------------------------------------------- /packages/rad-components/src/components/resourcenode/ResourceNode.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Resource } from '../../graph'; 3 | import { Handle, NodeProps, Position } from 'reactflow'; 4 | 5 | // Note: the default style assigned to a node gives it a 150px width 6 | // from style: .react-flow__node-default. 7 | 8 | export type ResourceNodeProps = Pick, 'data'>; 9 | 10 | function ResourceNode(props: ResourceNodeProps) { 11 | return ( 12 | <> 13 | 14 |
15 |

{props.data.name}

16 |
17 |
{props.data.type}
18 |
19 | 20 | 21 | ); 22 | } 23 | 24 | export default ResourceNode; 25 | -------------------------------------------------------------------------------- /packages/rad-components/src/components/resourcenode/__docs__/Example.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import ResourceNode, { ResourceNodeProps } from '../ResourceNode'; 3 | import * as sampledata from '../../../sampledata'; 4 | import { ReactFlowProvider } from 'reactflow'; 5 | 6 | const Example: FC = ({ 7 | data = sampledata.ContainerResource, 8 | }) => { 9 | return ( 10 | 11 |
19 | 20 |
21 |
22 | ); 23 | }; 24 | 25 | export default Example; 26 | -------------------------------------------------------------------------------- /packages/rad-components/src/components/resourcenode/__docs__/ResourceNode.mdx: -------------------------------------------------------------------------------- 1 | import { Canvas, Meta } from '@storybook/blocks'; 2 | import Example from './Example.tsx'; 3 | import * as ResourceNode from './ResourceNode.stories.tsx'; 4 | 5 | 6 | 7 | # ResourceNode 8 | 9 | ResourceNode component for different types of resources. 10 | 11 | #### Example 12 | 13 | 14 | 15 | #### Arguments 16 | 17 | - **resource** _`Resource`_ - The resource to display. 18 | -------------------------------------------------------------------------------- /packages/rad-components/src/components/resourcenode/__docs__/ResourceNode.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import Example from './Example'; 3 | import * as sampledata from '../../../sampledata'; 4 | 5 | const meta: Meta = { 6 | title: 'ResourceNode', 7 | component: Example, 8 | }; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const Container: Story = { 14 | args: { 15 | data: sampledata.ContainerResource, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /packages/rad-components/src/components/resourcenode/__test__/ResourceNode.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '@testing-library/jest-dom'; 3 | import { render, screen } from '@testing-library/react'; 4 | import ResourceNode from '../ResourceNode'; 5 | import * as sampledata from '../../../sampledata'; 6 | import { ReactFlowProvider } from 'reactflow'; 7 | 8 | describe('ResourceNode component', () => { 9 | it('ResourceNode should render correctly', () => { 10 | const resource = sampledata.ContainerResource; 11 | render( 12 | 13 | 14 | , 15 | ); 16 | const name = screen.getByRole('heading', { name: resource.name }); 17 | expect(name).toBeInTheDocument(); 18 | const type = screen.getByRole('heading', { name: resource.type }); 19 | expect(type).toBeInTheDocument(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/rad-components/src/components/resourcenode/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ResourceNode } from './ResourceNode'; 2 | -------------------------------------------------------------------------------- /packages/rad-components/src/graph.ts: -------------------------------------------------------------------------------- 1 | export interface AppGraph { 2 | name: string; 3 | resources: Resource[]; 4 | } 5 | 6 | export interface Resource { 7 | id: string; 8 | name: string; 9 | type: string; 10 | provider: string; 11 | provisioningState: string; 12 | resources?: Resource[]; 13 | connections?: Connection[]; 14 | } 15 | 16 | export interface Connection { 17 | id: string; 18 | name: string; 19 | type: string; 20 | provider: string; 21 | direction: Direction; 22 | } 23 | 24 | export type Direction = 'Outbound' | 'Inbound'; 25 | -------------------------------------------------------------------------------- /packages/rad-components/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | export type { AppGraph as AppGraphData } from './graph'; 3 | 4 | export { parseResourceId } from './resourceId'; 5 | export type { ResourceId } from './resourceId'; 6 | -------------------------------------------------------------------------------- /packages/rad-components/src/resourceId.ts: -------------------------------------------------------------------------------- 1 | const RESOURCE_ID_REGEX = 2 | /^\/planes\/radius\/(?[0-9a-zA-Z-]+)\/resourceGroups\/(?[0-9a-zA-Z-]+)\/providers\/(?[a-zA-Z\\.]+)\/(?[a-zA-Z]+)\/(?[0-9a-zA-Z-]+)$/i; 3 | 4 | export interface ResourceId { 5 | plane: string; 6 | group: string; 7 | type: string; 8 | name: string; 9 | } 10 | 11 | export function parseResourceId(resourceId: string): ResourceId | undefined { 12 | const match = RESOURCE_ID_REGEX.exec(resourceId); 13 | if (!match) { 14 | return undefined; 15 | } 16 | 17 | const { plane, group, namespace, type, name } = match.groups ?? {}; 18 | return { plane, group, type: `${namespace}/${type}`, name }; 19 | } 20 | -------------------------------------------------------------------------------- /packages/rad-components/src/sampledata.ts: -------------------------------------------------------------------------------- 1 | import { AppGraph, Resource } from './graph'; 2 | 3 | export const DemoApplication: AppGraph = { 4 | name: 'demo', 5 | resources: [ 6 | { 7 | id: '/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/containers/webapp', 8 | name: 'webapp', 9 | type: 'Applications.Core/container', 10 | provider: 'radius', 11 | provisioningState: 'Succeeded', 12 | connections: [ 13 | { 14 | id: '/planes/radius/local/resourceGroups/test-group/providers/Applications.Datastores/redisCaches/db', 15 | name: 'db', 16 | type: 'Applications.Datastores/redisCaches', 17 | provider: 'radius', 18 | direction: 'Outbound', 19 | }, 20 | ], 21 | }, 22 | { 23 | id: '/planes/radius/local/resourceGroups/test-group/providers/Applications.Datastores/redisCaches/db', 24 | name: 'db', 25 | type: 'Applications.Datastores/redisCaches', 26 | provider: 'radius', 27 | provisioningState: 'Succeeded', 28 | }, 29 | ], 30 | }; 31 | 32 | export const ContainerResource: Resource = { 33 | id: '/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/containers/test-container', 34 | name: 'test-container', 35 | type: 'Applications.Core/container', 36 | provider: 'radius', 37 | provisioningState: 'Succeeded', 38 | }; 39 | -------------------------------------------------------------------------------- /packages/rad-components/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { ResizeObserver } from '@juggle/resize-observer'; 3 | 4 | // Polyfill for ResizeObserver is required for react-flow in tests. 5 | global.ResizeObserver = ResizeObserver; 6 | window.ResizeObserver = ResizeObserver; 7 | -------------------------------------------------------------------------------- /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 } 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 | timeout: 60_000, 25 | 26 | expect: { 27 | timeout: 5_000, 28 | }, 29 | 30 | // Run your local dev server before starting the tests 31 | webServer: { 32 | command: 'yarn start', 33 | port: 3000, 34 | reuseExistingServer: true, 35 | timeout: 60_000, 36 | }, 37 | 38 | forbidOnly: !!process.env.CI, 39 | 40 | retries: process.env.CI ? 2 : 0, 41 | 42 | reporter: [ 43 | ['html', { open: 'never', outputFolder: './logs/e2e-test-report' }], 44 | ], 45 | 46 | use: { 47 | actionTimeout: 0, 48 | baseURL: process.env.PLAYWRIGHT_URL ?? 'http://localhost:3000', 49 | screenshot: 'only-on-failure', 50 | trace: 'on-first-retry', 51 | }, 52 | 53 | outputDir: './logs/e2e-test-results', 54 | 55 | projects: generateProjects(), // Find all packages with e2e-test folders 56 | }); 57 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # The Plugins Folder 2 | 3 | This is where your own plugins and their associated modules live, each in a 4 | separate folder of its own. 5 | 6 | If you want to create a new plugin here, go to your project root directory, run 7 | the command `yarn new`, and follow the on-screen instructions. 8 | 9 | You can also check out existing plugins on [the plugin marketplace](https://backstage.io/plugins)! 10 | -------------------------------------------------------------------------------- /plugins/plugin-radius-backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 2 | -------------------------------------------------------------------------------- /plugins/plugin-radius-backend/README.md: -------------------------------------------------------------------------------- 1 | # radius-backend 2 | 3 | Welcome to the radius backend plugin! 4 | 5 | _This plugin was created through the Backstage CLI_ 6 | 7 | ## Getting started 8 | 9 | Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn 10 | start` in the root directory, and then navigating to [/radius](http://localhost:3000/radius). 11 | 12 | You can also serve the plugin in isolation by running `yarn start` in the plugin directory. 13 | This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. 14 | It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. 15 | -------------------------------------------------------------------------------- /plugins/plugin-radius-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/plugin-radius-backend", 3 | "version": "0.1.0", 4 | "main": "src/index.ts", 5 | "types": "src/index.ts", 6 | "license": "Apache-2.0", 7 | "private": true, 8 | "packageManager": "yarn@4.0.2", 9 | "publishConfig": { 10 | "access": "public", 11 | "main": "dist/index.cjs.js", 12 | "types": "dist/index.d.ts" 13 | }, 14 | "backstage": { 15 | "role": "backend-plugin" 16 | }, 17 | "scripts": { 18 | "start": "backstage-cli package start", 19 | "build": "yarn exec backstage-cli package build", 20 | "lint": "backstage-cli package lint", 21 | "test": "backstage-cli package test", 22 | "clean": "backstage-cli package clean", 23 | "prepack": "backstage-cli package prepack", 24 | "postpack": "backstage-cli package postpack" 25 | }, 26 | "dependencies": { 27 | "@backstage/backend-common": "^0.20.2", 28 | "@backstage/config": "^1.1.1", 29 | "@types/express": "*", 30 | "express": "^4.19.2", 31 | "express-promise-router": "^4.1.0", 32 | "node-fetch": "^2.6.7", 33 | "winston": "^3.2.1", 34 | "yn": "^4.0.0" 35 | }, 36 | "devDependencies": { 37 | "@backstage/cli": "^0.25.0", 38 | "@types/supertest": "^2.0.12", 39 | "msw": "^1.0.0", 40 | "supertest": "^6.2.4" 41 | }, 42 | "files": [ 43 | "dist" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /plugins/plugin-radius-backend/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './service/router'; 2 | -------------------------------------------------------------------------------- /plugins/plugin-radius-backend/src/run.ts: -------------------------------------------------------------------------------- 1 | import { getRootLogger } from '@backstage/backend-common'; 2 | import yn from 'yn'; 3 | import { startStandaloneServer } from './service/standaloneServer'; 4 | 5 | const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; 6 | const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); 7 | const logger = getRootLogger(); 8 | 9 | startStandaloneServer({ port, enableCors, logger }).catch(err => { 10 | logger.error(err); 11 | process.exit(1); 12 | }); 13 | 14 | process.on('SIGINT', () => { 15 | logger.info('CTRL+C pressed; exiting.'); 16 | process.exit(0); 17 | }); 18 | -------------------------------------------------------------------------------- /plugins/plugin-radius-backend/src/service/router.test.ts: -------------------------------------------------------------------------------- 1 | import { getVoidLogger } from '@backstage/backend-common'; 2 | import express from 'express'; 3 | import request from 'supertest'; 4 | 5 | import { createRouter } from './router'; 6 | 7 | describe('createRouter', () => { 8 | let app: express.Express; 9 | 10 | beforeAll(async () => { 11 | const router = await createRouter({ 12 | logger: getVoidLogger(), 13 | }); 14 | app = express().use(router); 15 | }); 16 | 17 | beforeEach(() => { 18 | jest.resetAllMocks(); 19 | }); 20 | 21 | describe('GET /health', () => { 22 | it('returns ok', async () => { 23 | const response = await request(app).get('/health'); 24 | 25 | expect(response.status).toEqual(200); 26 | expect(response.body).toEqual({ status: 'ok' }); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /plugins/plugin-radius-backend/src/service/router.ts: -------------------------------------------------------------------------------- 1 | import { errorHandler } from '@backstage/backend-common'; 2 | import * as express from 'express'; 3 | import Router from 'express-promise-router'; 4 | import { Logger } from 'winston'; 5 | 6 | export interface RouterOptions { 7 | logger: Logger; 8 | } 9 | 10 | export async function createRouter( 11 | options: RouterOptions, 12 | ): Promise { 13 | const { logger } = options; 14 | 15 | const router = Router(); 16 | router.use(express.json()); 17 | 18 | router.get('/health', (_, response) => { 19 | logger.info('PONG!'); 20 | response.json({ status: 'ok' }); 21 | }); 22 | router.use(errorHandler()); 23 | return router; 24 | } 25 | -------------------------------------------------------------------------------- /plugins/plugin-radius-backend/src/service/standaloneServer.ts: -------------------------------------------------------------------------------- 1 | import { createServiceBuilder } from '@backstage/backend-common'; 2 | import { Server } from 'http'; 3 | import { Logger } from 'winston'; 4 | import { createRouter } from './router'; 5 | 6 | export interface ServerOptions { 7 | port: number; 8 | enableCors: boolean; 9 | logger: Logger; 10 | } 11 | 12 | export async function startStandaloneServer( 13 | options: ServerOptions, 14 | ): Promise { 15 | const logger = options.logger.child({ service: 'radius' }); 16 | logger.debug('Starting application server...'); 17 | const router = await createRouter({ 18 | logger, 19 | }); 20 | 21 | let service = createServiceBuilder(module) 22 | .setPort(options.port) 23 | .addRouter('/radius', router); 24 | if (options.enableCors) { 25 | service = service.enableCors({ origin: 'http://localhost:3000' }); 26 | } 27 | 28 | return await service.start().catch(err => { 29 | logger.error(err); 30 | process.exit(1); 31 | }); 32 | } 33 | 34 | module.hot?.accept(); 35 | -------------------------------------------------------------------------------- /plugins/plugin-radius-backend/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /plugins/plugin-radius/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 2 | -------------------------------------------------------------------------------- /plugins/plugin-radius/README.md: -------------------------------------------------------------------------------- 1 | # radius 2 | 3 | Welcome to the radius plugin! 4 | 5 | _This plugin was created through the Backstage CLI_ 6 | 7 | ## Getting started 8 | 9 | Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/radius](http://localhost:3000/radius). 10 | 11 | You can also serve the plugin in isolation by running `yarn start` in the plugin directory. 12 | This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. 13 | It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. 14 | -------------------------------------------------------------------------------- /plugins/plugin-radius/dev/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevApp } from '@backstage/dev-utils'; 3 | import { radiusPlugin, EnvironmentPage } from '../src/plugin'; 4 | 5 | createDevApp() 6 | .registerPlugin(radiusPlugin) 7 | .addPage({ 8 | element: , 9 | title: 'Root Page', 10 | path: '/radius', 11 | }) 12 | .render(); 13 | -------------------------------------------------------------------------------- /plugins/plugin-radius/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /plugins/plugin-radius/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/plugin-radius", 3 | "version": "0.1.0", 4 | "main": "src/index.ts", 5 | "types": "src/index.ts", 6 | "license": "Apache-2.0", 7 | "private": true, 8 | "publishConfig": { 9 | "access": "public", 10 | "main": "dist/index.esm.js", 11 | "types": "dist/index.d.ts" 12 | }, 13 | "backstage": { 14 | "role": "frontend-plugin" 15 | }, 16 | "sideEffects": false, 17 | "scripts": { 18 | "start": "backstage-cli package start", 19 | "build": "backstage-cli package build", 20 | "lint": "backstage-cli package lint", 21 | "test": "backstage-cli package test", 22 | "clean": "backstage-cli package clean", 23 | "prepack": "backstage-cli package prepack", 24 | "postpack": "backstage-cli package postpack" 25 | }, 26 | "dependencies": { 27 | "@backstage/core-components": "^0.13.9", 28 | "@backstage/core-plugin-api": "^1.8.1", 29 | "@backstage/plugin-kubernetes": "^0.11.3", 30 | "@backstage/theme": "^0.5.0", 31 | "@date-io/core": "^1.3.13", 32 | "@material-table/core": "^5.0.0", 33 | "@material-ui/core": "^4.9.13", 34 | "@material-ui/icons": "^4.9.1", 35 | "@material-ui/lab": "^4.0.0-alpha.61", 36 | "@radapp.io/rad-components": "^0.0.8", 37 | "react-error-boundary": "^4.0.12", 38 | "react-use": "^17.2.4" 39 | }, 40 | "peerDependencies": { 41 | "react": "^16.13.1 || ^17.0.2", 42 | "react-dom": "^16.13.1 || ^17.0.2", 43 | "react-router-dom": "^6.3.0" 44 | }, 45 | "devDependencies": { 46 | "@backstage/cli": "^0.25.0", 47 | "@backstage/core-app-api": "^1.11.2", 48 | "@backstage/dev-utils": "^1.0.25", 49 | "@backstage/plugin-kubernetes-common": "^0.7.2", 50 | "@backstage/test-utils": "^1.4.6", 51 | "@testing-library/dom": "^8.0.0", 52 | "@testing-library/jest-dom": "^5.10.1", 53 | "@testing-library/react": "^12.1.3", 54 | "@testing-library/user-event": "^14.0.0", 55 | "jest-canvas-mock": "^2.5.2", 56 | "msw": "^1.0.0" 57 | }, 58 | "files": [ 59 | "dist" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/api/api.test.ts: -------------------------------------------------------------------------------- 1 | import { makePath, makePathForId, RadiusApiImpl } from './api'; 2 | 3 | describe('makePath', () => { 4 | it('makes path for scopes', () => { 5 | const path = makePath({ scopes: [{ type: 'radius', value: 'local' }] }); 6 | expect(path).toEqual(makePathForId('/planes/radius/local')); 7 | }); 8 | it('makes path for multiple scopes', () => { 9 | const path = makePath({ 10 | scopes: [ 11 | { type: 'radius', value: 'local' }, 12 | { 13 | type: 'resourceGroups', 14 | value: 'test-group', 15 | }, 16 | ], 17 | }); 18 | expect(path).toEqual( 19 | makePathForId('/planes/radius/local/resourceGroups/test-group'), 20 | ); 21 | }); 22 | it('makes path for scope list', () => { 23 | const path = makePath({ 24 | scopes: [ 25 | { type: 'radius', value: 'local' }, 26 | { 27 | type: 'resourceGroups', 28 | }, 29 | ], 30 | }); 31 | expect(path).toEqual(makePathForId('/planes/radius/local/resourceGroups')); 32 | }); 33 | it('makes path for scope action', () => { 34 | const path = makePath({ 35 | scopes: [{ type: 'radius', value: 'local' }], 36 | action: 'action', 37 | }); 38 | expect(path).toEqual(makePathForId('/planes/radius/local/action')); 39 | }); 40 | it('makes path for resource type without name', () => { 41 | const path = makePath({ 42 | scopes: [{ type: 'radius', value: 'local' }], 43 | type: 'Applications.Core/applications', 44 | }); 45 | expect(path).toEqual( 46 | makePathForId( 47 | '/planes/radius/local/providers/Applications.Core/applications', 48 | ), 49 | ); 50 | }); 51 | it('makes path for resource type with name', () => { 52 | const path = makePath({ 53 | scopes: [{ type: 'radius', value: 'local' }], 54 | type: 'Applications.Core/applications', 55 | name: 'test-app', 56 | }); 57 | expect(path).toEqual( 58 | makePathForId( 59 | '/planes/radius/local/providers/Applications.Core/applications/test-app', 60 | ), 61 | ); 62 | }); 63 | it('makes path for resource type with name and action', () => { 64 | const path = makePath({ 65 | scopes: [{ type: 'radius', value: 'local' }], 66 | type: 'Applications.Core/applications', 67 | name: 'test-app', 68 | action: 'restart', 69 | }); 70 | expect(path).toEqual( 71 | makePathForId( 72 | '/planes/radius/local/providers/Applications.Core/applications/test-app/restart', 73 | ), 74 | ); 75 | }); 76 | }); 77 | 78 | describe('RadiusApi', () => { 79 | it('selectCluster returns first cluster', async () => { 80 | const api = new RadiusApiImpl({ 81 | getClusters: async () => [ 82 | { name: 'test-cluster1', authProvider: 'test' }, 83 | { 84 | name: 'test-cluster2', 85 | authProvider: 'test', 86 | }, 87 | ], 88 | proxy: async () => { 89 | throw new Error('not implemented'); 90 | }, 91 | }); 92 | // eslint-disable-next-line dot-notation 93 | expect(await api['selectCluster']()).toEqual('test-cluster1'); 94 | }); 95 | it('makeRequest handles errors', async () => { 96 | const api = new RadiusApiImpl({ 97 | getClusters: async () => { 98 | throw new Error('not implemented'); 99 | }, 100 | proxy: async () => Promise.resolve(new Response('test', { status: 404 })), 101 | }); 102 | // eslint-disable-next-line dot-notation 103 | await expect(api['makeRequest']('cluster', 'path')).rejects.toThrow( 104 | 'Request failed: 404:\n\ntest', 105 | ); 106 | }); 107 | it('makeRequest expects JSON', async () => { 108 | const api = new RadiusApiImpl({ 109 | getClusters: async () => { 110 | throw new Error('not implemented'); 111 | }, 112 | proxy: async () => Promise.resolve(new Response('test')), 113 | }); 114 | // eslint-disable-next-line dot-notation 115 | await expect(api['makeRequest']('cluster', 'path')).rejects.toThrow( 116 | 'invalid json response body at reason: Unexpected token \'e\', "test" is not valid JSON', 117 | ); 118 | }); 119 | it('makeRequest parses JSON', async () => { 120 | const api = new RadiusApiImpl({ 121 | getClusters: async () => { 122 | throw new Error('not implemented'); 123 | }, 124 | proxy: async () => Promise.resolve(new Response('{ "message": "test" }')), 125 | }); 126 | // eslint-disable-next-line dot-notation 127 | await expect(api['makeRequest']('cluster', 'path')).resolves.toEqual({ 128 | message: 'test', 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export type { RadiusApi } from './api'; 2 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/applications/ApplicationIcon.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ApplicationIcon from './ApplicationIcon'; 3 | import { render } from '@testing-library/react'; 4 | 5 | describe('ApplicationIcon', () => { 6 | it('should render the full logo by default', () => { 7 | const result = render(); 8 | const svg = result.baseElement.querySelector('svg'); 9 | expect(svg).toBeInTheDocument(); 10 | expect(svg).toHaveAttribute('viewBox', '0 0 1080 1080'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/applications/ApplicationIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SvgIcon } from '@material-ui/core'; 3 | 4 | export const ApplicationIcon = () => { 5 | const originalWidth = 1080; 6 | const originalHeight = 1080; 7 | 8 | return ( 9 | 10 | 11 | 15 | 19 | 23 | 24 | 28 | 32 | 36 | 37 | ); 38 | }; 39 | 40 | export default ApplicationIcon; 41 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/applications/ApplicationListInfoCard.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | InfoCard, 3 | LinkButton, 4 | Progress, 5 | ResponseErrorPanel, 6 | Table, 7 | TableColumn, 8 | } from '@backstage/core-components'; 9 | import { useApi, useRouteRef } from '@backstage/core-plugin-api'; 10 | import React from 'react'; 11 | import useAsync from 'react-use/lib/useAsync'; 12 | import { radiusApiRef } from '../../plugin'; 13 | import { ApplicationProperties, Resource, ResourceList } from '../../resources'; 14 | import { ResourceLink } from '../resourcelink'; 15 | import { resourcePageRouteRef } from '../../routes'; 16 | import { parseResourceId } from '@radapp.io/rad-components'; 17 | 18 | const ApplicationListInfoContent = () => { 19 | const route = useRouteRef(resourcePageRouteRef); 20 | 21 | const radiusApi = useApi(radiusApiRef); 22 | const { value, loading, error } = useAsync( 23 | async (): Promise> => { 24 | return radiusApi.listApplications(); 25 | }, 26 | ); 27 | 28 | if (loading) { 29 | return ; 30 | } else if (error) { 31 | return ; 32 | } 33 | 34 | const columns: TableColumn>[] = [ 35 | { 36 | title: 'Name', 37 | type: 'string', 38 | width: '30%', 39 | highlight: true, 40 | render: row => , 41 | }, 42 | { 43 | title: 'Actions', 44 | align: 'right', 45 | render: row => { 46 | const parsed = parseResourceId(row.id); 47 | if (!parsed) { 48 | return null; 49 | } 50 | 51 | const base = route({ 52 | group: parsed.group, 53 | namespace: parsed.type.split('/')[0], 54 | type: parsed.type.split('/')[1], 55 | name: parsed.name, 56 | }); 57 | return ( 58 | <> 59 | App Graph 60 | Resources 61 | 62 | ); 63 | }, 64 | }, 65 | ]; 66 | 67 | return ( 68 | null }} 71 | options={{ search: false, paging: false, padding: 'dense', pageSize: 5 }} 72 | data={value?.value || []} 73 | /> 74 | ); 75 | }; 76 | 77 | export const ApplicationListInfoCard = () => { 78 | return ( 79 | 80 | 81 | 82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/applications/ApplicationListPage.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ApplicationListPage } from './ApplicationListPage'; 3 | import { screen } from '@testing-library/react'; 4 | import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; 5 | import { RadiusApi } from '../../api'; 6 | import { radiusApiRef } from '../../plugin'; 7 | import { ApplicationProperties, ResourceList } from '../../resources'; 8 | 9 | describe('ApplicationListPage', () => { 10 | // Rendering an empty table is fine for now, we have good unit tests for the 11 | // table logic elsewhere. 12 | it('should render table', async () => { 13 | const api: Pick = { 14 | listResources: async () => 15 | Promise.resolve>({ 16 | value: [], 17 | }), 18 | }; 19 | 20 | await renderInTestApp( 21 | 22 | 23 | , 24 | ); 25 | expect( 26 | screen.getByText('Displaying deployed applications.'), 27 | ).toBeInTheDocument(); 28 | expect(screen.getByRole('table')).toBeInTheDocument(); 29 | 30 | const table = screen.getByRole('table'); 31 | expect(table).toBeInTheDocument(); 32 | 33 | const rows = screen.getAllByRole('row'); 34 | expect(rows).toHaveLength(2); // Header + empty row 35 | const [header] = rows; 36 | 37 | // Verify correct headings (we had headings that will never be shown for an application) 38 | const expectedColumns = [ 39 | 'Name', 40 | 'Resource Group', 41 | 'Type', 42 | 'Environment', 43 | 'Status', 44 | ]; 45 | const headings = header.querySelectorAll('th'); 46 | expect(headings).toHaveLength(expectedColumns.length); 47 | headings.forEach((heading, index) => { 48 | expect(heading).toHaveTextContent(expectedColumns[index]); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/applications/ApplicationListPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Grid, Typography, Box } from '@material-ui/core'; 3 | import { 4 | Header, 5 | Page, 6 | Content, 7 | Breadcrumbs, 8 | Link, 9 | } from '@backstage/core-components'; 10 | import { ResourceTable } from '../resourcetable'; 11 | 12 | export const ApplicationListPage = () => ( 13 | 14 |
15 | 16 | 17 | 18 | Home 19 | Environments 20 | Applications 21 | 22 | 23 | 24 | 25 | 29 | 30 | 31 | 32 | 33 | ); 34 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/applications/index.ts: -------------------------------------------------------------------------------- 1 | export { ApplicationListPage } from './ApplicationListPage'; 2 | export { ApplicationListInfoCard } from './ApplicationListInfoCard'; 3 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/environments/EnvironmentIcon.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import EnvironmentIcon from './EnvironmentIcon'; 3 | import { render } from '@testing-library/react'; 4 | 5 | describe('EnvironmentIcon', () => { 6 | it('should render the full logo by default', () => { 7 | const result = render(); 8 | const svg = result.baseElement.querySelector('svg'); 9 | expect(svg).toBeInTheDocument(); 10 | expect(svg).toHaveAttribute('viewBox', '0 0 1080 1080'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/environments/EnvironmentIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SvgIcon } from '@material-ui/core'; 3 | 4 | export const EnvironmentIcon = () => { 5 | const originalWidth = 1080; 6 | const originalHeight = 1080; 7 | 8 | return ( 9 | 10 | 14 | 18 | 19 | ); 20 | }; 21 | 22 | export default EnvironmentIcon; 23 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/environments/EnvironmentListInfoCard.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | InfoCard, 3 | LinkButton, 4 | Progress, 5 | ResponseErrorPanel, 6 | Table, 7 | TableColumn, 8 | } from '@backstage/core-components'; 9 | import { useApi, useRouteRef } from '@backstage/core-plugin-api'; 10 | import React from 'react'; 11 | import useAsync from 'react-use/lib/useAsync'; 12 | import { radiusApiRef } from '../../plugin'; 13 | import { EnvironmentProperties, Resource, ResourceList } from '../../resources'; 14 | import { ResourceLink } from '../resourcelink'; 15 | import { resourcePageRouteRef } from '../../routes'; 16 | import { parseResourceId } from '@radapp.io/rad-components'; 17 | 18 | const EnvironmentListInfoContent = () => { 19 | const route = useRouteRef(resourcePageRouteRef); 20 | 21 | const radiusApi = useApi(radiusApiRef); 22 | const { value, loading, error } = useAsync( 23 | async (): Promise> => { 24 | return radiusApi.listEnvironments(); 25 | }, 26 | ); 27 | 28 | if (loading) { 29 | return ; 30 | } else if (error) { 31 | return ; 32 | } 33 | 34 | const columns: TableColumn>[] = [ 35 | { 36 | title: 'Name', 37 | type: 'string', 38 | width: '30%', 39 | highlight: true, 40 | render: row => , 41 | }, 42 | { 43 | title: 'Actions', 44 | align: 'right', 45 | render: row => { 46 | const parsed = parseResourceId(row.id); 47 | if (!parsed) { 48 | return null; 49 | } 50 | 51 | const base = route({ 52 | group: parsed.group, 53 | namespace: parsed.type.split('/')[0], 54 | type: parsed.type.split('/')[1], 55 | name: parsed.name, 56 | }); 57 | return ( 58 | <> 59 | Recipes 60 | Resources 61 | 62 | ); 63 | }, 64 | }, 65 | ]; 66 | 67 | return ( 68 |
null }} 71 | options={{ search: false, paging: false, padding: 'dense', pageSize: 5 }} 72 | data={value?.value || []} 73 | /> 74 | ); 75 | }; 76 | 77 | export const EnvironmentListInfoCard = () => { 78 | return ( 79 | 80 | 81 | 82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/environments/EnvironmentListPage.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { EnvironmentListPage } from './EnvironmentListPage'; 3 | import { screen } from '@testing-library/react'; 4 | import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; 5 | import { RadiusApi } from '../../api'; 6 | import { radiusApiRef } from '../../plugin'; 7 | import { EnvironmentProperties, ResourceList } from '../../resources'; 8 | 9 | describe('EnvironmentListPage', () => { 10 | // Rendering an empty table is fine for now, we have good unit tests for the 11 | // table logic elsewhere. 12 | it('should render table', async () => { 13 | const api: Pick = { 14 | listResources: async () => 15 | Promise.resolve>({ 16 | value: [], 17 | }), 18 | }; 19 | 20 | await renderInTestApp( 21 | 22 | 23 | , 24 | ); 25 | expect( 26 | screen.getByText( 27 | 'Displaying environments where applications can be deployed.', 28 | ), 29 | ).toBeInTheDocument(); 30 | expect(screen.getByRole('table')).toBeInTheDocument(); 31 | 32 | const table = screen.getByRole('table'); 33 | expect(table).toBeInTheDocument(); 34 | 35 | const rows = screen.getAllByRole('row'); 36 | expect(rows).toHaveLength(2); // Header + empty row 37 | const [header] = rows; 38 | 39 | // Verify correct headings (we had headings that will never be shown for an environment) 40 | const expectedColumns = ['Name', 'Resource Group', 'Type', 'Status']; 41 | const headings = header.querySelectorAll('th'); 42 | expect(headings).toHaveLength(expectedColumns.length); 43 | headings.forEach((heading, index) => { 44 | expect(heading).toHaveTextContent(expectedColumns[index]); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/environments/EnvironmentListPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Grid, Typography, Box } from '@material-ui/core'; 3 | import { 4 | Header, 5 | Page, 6 | Content, 7 | Breadcrumbs, 8 | Link, 9 | } from '@backstage/core-components'; 10 | import { ResourceTable } from '../resourcetable'; 11 | 12 | export const EnvironmentListPage = () => ( 13 | 14 |
18 | 19 | 20 | 21 | Home 22 | Environments 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | ); 36 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/environments/index.ts: -------------------------------------------------------------------------------- 1 | export { EnvironmentListPage } from './EnvironmentListPage'; 2 | export { EnvironmentListInfoCard } from './EnvironmentListInfoCard'; 3 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/logo/RadiusLogo.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RadiusLogo from './RadiusLogo'; 3 | import { render } from '@testing-library/react'; 4 | 5 | describe('RadiusLogo', () => { 6 | it('should render the full logo by default', () => { 7 | const result = render(); 8 | const svg = result.baseElement.querySelector('svg'); 9 | expect(svg).toBeInTheDocument(); 10 | expect(svg).toHaveAttribute('viewBox', '0 0 3500 950'); 11 | }); 12 | 13 | it('should render the full logo when asked', () => { 14 | const result = render(); 15 | const svg = result.baseElement.querySelector('svg'); 16 | expect(svg).toBeInTheDocument(); 17 | expect(svg).toHaveAttribute('viewBox', '0 0 3500 950'); 18 | }); 19 | 20 | it('should render the square logo when asked', () => { 21 | const result = render(); 22 | const svg = result.baseElement.querySelector('svg'); 23 | expect(svg).toBeInTheDocument(); 24 | expect(svg).toHaveAttribute('viewBox', '0 0 950 950'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/logo/RadiusLogomarkReverse.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RadiusLogomarkReverse from './RadiusLogomarkReverse'; 3 | import { render } from '@testing-library/react'; 4 | 5 | describe('RadiusLogomarkReverse', () => { 6 | it('should render the full logo by default', () => { 7 | const result = render(); 8 | const svg = result.baseElement.querySelector('svg'); 9 | expect(svg).toBeInTheDocument(); 10 | expect(svg).toHaveAttribute('viewBox', '175 175 550 550'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/logo/RadiusLogomarkReverse.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SvgIcon } from '@material-ui/core'; 3 | 4 | export const RadiusLogomarkReverse = () => { 5 | const originalWidth = 550; 6 | const originalHeight = 550; 7 | 8 | return ( 9 | 10 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default RadiusLogomarkReverse; 20 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/recipes/RecipeIcon.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RecipeIcon from './RecipeIcon'; 3 | import { render } from '@testing-library/react'; 4 | 5 | describe('RecipeIcon', () => { 6 | it('should render the full logo by default', () => { 7 | const result = render(); 8 | const svg = result.baseElement.querySelector('svg'); 9 | expect(svg).toBeInTheDocument(); 10 | expect(svg).toHaveAttribute('viewBox', '0 0 1080 1080'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/recipes/RecipeIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SvgIcon } from '@material-ui/core'; 3 | 4 | export const RecipeIcon = () => { 5 | const originalWidth = 1080; 6 | const originalHeight = 1080; 7 | 8 | return ( 9 | 10 | 14 | 15 | 19 | 23 | 24 | 28 | 32 | 33 | 34 | 38 | 42 | 43 | 44 | 48 | 52 | 53 | 57 | 61 | 62 | 63 | ); 64 | }; 65 | 66 | export default RecipeIcon; 67 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/recipes/RecipeListPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | FormControl, 4 | Grid, 5 | InputLabel, 6 | MenuItem, 7 | Select, 8 | Typography, 9 | } from '@material-ui/core'; 10 | import { 11 | Header, 12 | Page, 13 | Content, 14 | Progress, 15 | ResponseErrorPanel, 16 | } from '@backstage/core-components'; 17 | import { radiusApiRef } from '../../plugin'; 18 | import { EnvironmentProperties, Resource, ResourceList } from '../../resources'; 19 | import { useApi } from '@backstage/core-plugin-api'; 20 | import useAsync from 'react-use/lib/useAsync'; 21 | import { RecipeTable } from './RecipeTable'; 22 | 23 | export const RecipeListPageContent2 = ({ 24 | environments, 25 | }: { 26 | environments: Resource[]; 27 | }) => { 28 | const first = environments.length > 0 ? environments[0].id : undefined; 29 | 30 | const [selected, setSelected] = useState(first); 31 | 32 | const env = environments.find(e => e.id === selected); 33 | 34 | return ( 35 | <> 36 | 37 | 38 | Environment 39 | 40 | 53 | 54 | 55 | 56 | {env ? ( 57 | 58 | ) : ( 59 | 60 | Select an environment to display recipes. 61 | 62 | )} 63 | 64 | 65 | ); 66 | }; 67 | 68 | export const RecipeListPageContent = () => { 69 | const radiusApi = useApi(radiusApiRef); 70 | const { value, loading, error } = useAsync( 71 | async (): Promise> => 72 | radiusApi.listEnvironments(), 73 | [], 74 | ); 75 | 76 | if (loading) { 77 | return ; 78 | } else if (error) { 79 | return ; 80 | } 81 | 82 | if (!value) { 83 | throw new Error('This should not happen.'); 84 | } 85 | 86 | return ; 87 | }; 88 | 89 | export const RecipeListPage = () => { 90 | return ( 91 | 92 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | ); 103 | }; 104 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/recipes/RecipeTable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Table, TableColumn, TableFilter } from '@backstage/core-components'; 3 | import { EnvironmentProperties, Resource } from '../../resources'; 4 | 5 | interface DisplayRecipe { 6 | name: string; 7 | type: string; 8 | templatePath: string; 9 | templateKind: string; 10 | } 11 | 12 | export const RecipeTable = ({ 13 | environment, 14 | }: { 15 | environment: Resource; 16 | title?: string; 17 | }) => { 18 | const raw = environment.properties?.recipes; 19 | 20 | let recipes: DisplayRecipe[] = []; 21 | 22 | // Recipes are stored two-levels of nested object, so we need to flatten them out. 23 | // First level is the recipe type, second level is the recipe name. 24 | if (raw) { 25 | recipes = Object.keys(raw).flatMap(recipeType => 26 | Object.keys(raw[recipeType]).map(recipeName => { 27 | return { 28 | type: recipeType, 29 | name: recipeName, 30 | ...raw[recipeType][recipeName], 31 | }; 32 | }), 33 | ); 34 | } 35 | 36 | const columns: TableColumn[] = [ 37 | { title: 'Name', field: 'name' }, 38 | { title: 'Type', field: 'type' }, 39 | { title: 'Kind', field: 'templateKind' }, 40 | { title: 'Template Path', field: 'templatePath' }, 41 | ]; 42 | 43 | const filters: TableFilter[] = [ 44 | { 45 | column: 'Name', 46 | type: 'multiple-select', 47 | }, 48 | { 49 | column: 'Type', 50 | type: 'multiple-select', 51 | }, 52 | { 53 | column: 'Kind', 54 | type: 'multiple-select', 55 | }, 56 | { 57 | column: 'Template Path', 58 | type: 'multiple-select', 59 | }, 60 | ]; 61 | 62 | return ( 63 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/recipes/index.ts: -------------------------------------------------------------------------------- 1 | export { RecipeListPage } from './RecipeListPage'; 2 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resourcebreadcrumbs/ResourceBreadcrumbs.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderInTestApp } from '@backstage/test-utils'; 2 | import { screen } from '@testing-library/react'; 3 | import { ResourceBreadcrumbs } from './ResourceBreadcrumbs'; 4 | import React from 'react'; 5 | import { Resource } from '../../resources'; 6 | import { resourcePageRouteRef } from '../../routes'; 7 | 8 | describe('ResourceBreadcrumbs', () => { 9 | it('should render breadcrumbs', async () => { 10 | const resource: Resource<{ [key: string]: unknown }> = { 11 | id: '/planes/radius/local/resourcegroups/default/providers/Applications.Datastores/redisCaches/uiCache', 12 | name: 'uiCache', 13 | type: 'Applications.Datastores/redisCaches', 14 | properties: { 15 | application: 16 | '/planes/radius/local/resourceGroups/default/providers/applications.core/applications/dashboard-app', 17 | environment: 18 | '/planes/radius/local/resourceGroups/default/providers/applications.core/environments/default', 19 | }, 20 | systemData: {}, 21 | }; 22 | const rendered = await renderInTestApp( 23 | , 24 | { 25 | mountedRoutes: { 26 | '/resource/:group/:namespace/:type/:name': resourcePageRouteRef, 27 | }, 28 | }, 29 | ); 30 | expect(rendered.getByText('default')).toBeVisible(); 31 | expect(screen.getByRole('link', { name: 'default' })).toHaveAttribute( 32 | 'href', 33 | '/resource/default/applications.core/environments/default', 34 | ); 35 | expect(rendered.getByText('dashboard-app')).toBeVisible(); 36 | expect(screen.getByRole('link', { name: 'dashboard-app' })).toHaveAttribute( 37 | 'href', 38 | '/resource/default/applications.core/applications/dashboard-app', 39 | ); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resourcebreadcrumbs/ResourceBreadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Resource } from '../../resources'; 3 | import { ResourceLink } from '../resourcelink'; 4 | import { Breadcrumbs } from '@backstage/core-components'; 5 | import { Typography } from '@material-ui/core'; 6 | 7 | export const ResourceBreadcrumbs = (props: { resource: Resource }) => { 8 | return ( 9 | 10 | {props.resource.properties?.environment && ( 11 | 12 | )} 13 | {props.resource.properties?.application && ( 14 | 15 | )} 16 | {props.resource.name} 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resourcebreadcrumbs/index.ts: -------------------------------------------------------------------------------- 1 | export { ResourceBreadcrumbs } from './ResourceBreadcrumbs'; 2 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resourcelink/ResourceLink.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { screen, waitFor } from '@testing-library/react'; 3 | import { ResourceLink } from './ResourceLink'; 4 | import { renderInTestApp } from '@backstage/test-utils'; 5 | import { resourcePageRouteRef } from '../../routes'; 6 | import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; 7 | 8 | describe('ResourceLink', () => { 9 | it('should render', async () => { 10 | const id = 11 | '/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/test-environment'; 12 | await renderInTestApp(Hi There!, { 13 | mountedRoutes: { 14 | '/resource/:group/:namespace/:type/:name': resourcePageRouteRef, 15 | }, 16 | }); 17 | expect(screen.getByRole('link', { name: 'Hi There!' })).toHaveAttribute( 18 | 'href', 19 | '/resource/test-group/Applications.Core/environments/test-environment', 20 | ); 21 | }); 22 | 23 | it('should render the provided resource name', async () => { 24 | const id = 25 | '/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/test-environment'; 26 | await renderInTestApp(, { 27 | mountedRoutes: { 28 | '/resource/:group/:namespace/:type/:name': resourcePageRouteRef, 29 | }, 30 | }); 31 | expect( 32 | screen.getByRole('link', { name: 'test-environment' }), 33 | ).toHaveAttribute( 34 | 'href', 35 | '/resource/test-group/Applications.Core/environments/test-environment', 36 | ); 37 | }); 38 | 39 | it('should throw for invalid resource id', async () => { 40 | const id = 41 | '/planes/radius/local/resourceGroups/test-group/providers/Applications.Cor12323231e-----/environments'; 42 | const fallbackRender = jest.fn((_: FallbackProps) => null); 43 | await renderInTestApp( 44 | 45 | Should not render 46 | , 47 | { 48 | mountedRoutes: { 49 | '/resource/:group/:namespace/:type/:name': resourcePageRouteRef, 50 | }, 51 | }, 52 | ); 53 | 54 | await waitFor(() => expect(fallbackRender).toHaveBeenCalled()); 55 | expect(fallbackRender.mock.calls[0][0].error).toEqual( 56 | new Error(`Invalid resource id ${id}`), 57 | ); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resourcelink/ResourceLink.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from 'react'; 2 | import { Link } from '@backstage/core-components'; 3 | import { parseResourceId } from '@radapp.io/rad-components'; 4 | import { useRouteRef } from '@backstage/core-plugin-api'; 5 | import { resourcePageRouteRef } from '../../routes'; 6 | 7 | export const ResourceLink = (props: PropsWithChildren<{ id: string }>) => { 8 | const parsed = parseResourceId(props.id); 9 | if (!parsed) { 10 | throw new Error(`Invalid resource id ${props.id}`); 11 | } 12 | 13 | const route = useRouteRef(resourcePageRouteRef); 14 | 15 | return ( 16 | 24 | {props.children ?? parsed.name} 25 | 26 | ); 27 | }; 28 | 29 | export const OptionalResourceLink = ( 30 | props: PropsWithChildren<{ id?: string }>, 31 | ) => { 32 | if (!props.id) { 33 | return null; 34 | } 35 | 36 | return {props.children}; 37 | }; 38 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resourcelink/index.ts: -------------------------------------------------------------------------------- 1 | export { ResourceLink, OptionalResourceLink } from './ResourceLink'; 2 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resources/ApplicationResourcesTab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box } from '@material-ui/core'; 3 | import { Resource } from '../../resources'; 4 | import { ResourceTable } from '../resourcetable'; 5 | import { ResourceBreadcrumbs } from '../resourcebreadcrumbs'; 6 | 7 | export const ApplicationResourcesTab = ({ 8 | resource, 9 | }: { 10 | resource: Resource; 11 | }) => { 12 | return ( 13 | <> 14 | 15 | 16 | 17 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resources/ApplicationTab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { parseResourceId } from '@radapp.io/rad-components'; 3 | import { 4 | InfoCard, 5 | Progress, 6 | ResponseErrorPanel, 7 | } from '@backstage/core-components'; 8 | import { kubernetesApiRef } from '@backstage/plugin-kubernetes'; 9 | import { useApi } from '@backstage/core-plugin-api'; 10 | import useAsync from 'react-use/lib/useAsync'; 11 | import { AppGraph } from '@radapp.io/rad-components'; 12 | import { makeStyles } from '@material-ui/core'; 13 | 14 | export interface AppGraphData { 15 | name: string; 16 | resources: ResourceData[]; 17 | } 18 | 19 | export interface ResourceData { 20 | id: string; 21 | name: string; 22 | type: string; 23 | provider: string; 24 | provisioningState: string; 25 | resources?: ResourceData[]; 26 | connections?: Connection[]; 27 | } 28 | 29 | export interface Connection { 30 | id: string; 31 | name: string; 32 | type: string; 33 | provider: string; 34 | direction: Direction; 35 | } 36 | 37 | export type Direction = 'Outbound' | 'Inbound'; 38 | 39 | const useStyles = makeStyles({ 40 | container: { 41 | height: '500px', 42 | width: '100%', 43 | }, 44 | }); 45 | 46 | export const ApplicationTab = ({ application }: { application: string }) => { 47 | const styles = useStyles(); 48 | const kubernetesApi = useApi(kubernetesApiRef); 49 | const { value, loading, error } = 50 | useAsync(async (): Promise => { 51 | let first = ''; 52 | const clusters = await kubernetesApi.getClusters(); 53 | for (const cluster of clusters) { 54 | first = cluster.name; 55 | } 56 | 57 | const response = await kubernetesApi.proxy({ 58 | clusterName: first, 59 | path: `/apis/api.ucp.dev/v1alpha3/${application}/getGraph?api-version=2023-10-01-preview`, 60 | init: { 61 | referrerPolicy: 'no-referrer', 62 | mode: 'cors', 63 | cache: 'no-cache', 64 | method: 'POST', 65 | }, 66 | }); 67 | 68 | if (!response.ok) { 69 | const text = await response.text(); 70 | throw new Error(`Request failed: ${response.status}:\n\n ${text}`); 71 | } 72 | 73 | return (await response.json()) as AppGraphData; 74 | }, [application]); 75 | 76 | if (loading || !value) { 77 | return ; 78 | } else if (error) { 79 | return ; 80 | } 81 | 82 | return ( 83 | <> 84 | 87 |
88 | 89 |
90 |
91 | 92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resources/DetailsTab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box } from '@material-ui/core'; 3 | import { InfoCard } from '@backstage/core-components'; 4 | import { Resource } from '../../resources'; 5 | import { ResourceBreadcrumbs } from '../resourcebreadcrumbs'; 6 | 7 | export const DetailsTab = (props: { resource: Resource }) => { 8 | return ( 9 | <> 10 | 11 | 12 | 13 | 14 |
{JSON.stringify(props.resource, null, 2)}
15 |
16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resources/EnvironmentResourcesTab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Resource } from '../../resources'; 3 | import { ResourceTable } from '../resourcetable'; 4 | 5 | export const EnvironmentResourcesTab = ({ 6 | resource, 7 | }: { 8 | resource: Resource; 9 | }) => { 10 | return ( 11 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resources/OverviewTab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Resource } from '../../resources'; 3 | import { InfoCard, StructuredMetadataTable } from '@backstage/core-components'; 4 | import { Box } from '@material-ui/core'; 5 | import { ResourceLink } from '../resourcelink/ResourceLink'; 6 | import { ResourceBreadcrumbs } from '../resourcebreadcrumbs'; 7 | import { parseResourceId } from '@radapp.io/rad-components'; 8 | 9 | export const OverviewTab = (props: { resource: Resource }) => { 10 | const metadata: { [key: string]: unknown } = { 11 | name: props.resource.name, 12 | type: props.resource.type, 13 | group: parseResourceId(props.resource.id)?.group, 14 | }; 15 | 16 | if (props.resource.properties?.environment as string) { 17 | metadata.environment = ( 18 | 19 | ); 20 | } 21 | if (props.resource.properties?.application as string) { 22 | metadata.application = ( 23 | 24 | ); 25 | } 26 | 27 | return ( 28 | <> 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resources/RecipesTab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { EnvironmentProperties, Resource } from '../../resources'; 3 | import { RecipeTable } from '../recipes/RecipeTable'; 4 | 5 | export const RecipesTab = ({ 6 | resource, 7 | }: { 8 | resource: Resource; 9 | }) => { 10 | return ; 11 | }; 12 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resources/ResourceIcon.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ResourceIcon from './ResourceIcon'; 3 | import { render } from '@testing-library/react'; 4 | 5 | describe('ResourceIcon', () => { 6 | it('should render the full logo by default', () => { 7 | const result = render(); 8 | const svg = result.baseElement.querySelector('svg'); 9 | expect(svg).toBeInTheDocument(); 10 | expect(svg).toHaveAttribute('viewBox', '0 0 1080 1080'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resources/ResourceIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SvgIcon } from '@material-ui/core'; 3 | 4 | export const ResourceIcon = () => { 5 | const originalWidth = 1080; 6 | const originalHeight = 1080; 7 | 8 | return ( 9 | 10 | 14 | 15 | ); 16 | }; 17 | 18 | export default ResourceIcon; 19 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resources/ResourceLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Content, Header, Page } from '@backstage/core-components'; 2 | import { useRouteRefParams } from '@backstage/core-plugin-api'; 3 | import { Grid } from '@material-ui/core'; 4 | import React, { PropsWithChildren } from 'react'; 5 | import { resourcePageRouteRef } from '../../routes'; 6 | import { Resource } from '../../resources'; 7 | 8 | export const ResourceLayout = ( 9 | props: PropsWithChildren<{ resource: Resource }>, 10 | ) => { 11 | const params = useRouteRefParams(resourcePageRouteRef); 12 | 13 | return ( 14 | 15 |
19 | 20 | 21 | {props.children} 22 | 23 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resources/ResourceListPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Grid } from '@material-ui/core'; 3 | import { Header, Page, Content } from '@backstage/core-components'; 4 | import { ResourceTable } from '../resourcetable'; 5 | 6 | export const ResourceListPage = () => ( 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resources/ResourcePage.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; 3 | import { screen, waitFor } from '@testing-library/react'; 4 | import { RadiusApi } from '../../api'; 5 | import { radiusApiRef } from '../../plugin'; 6 | import { Resource } from '../../resources'; 7 | import { ResourcePage } from './ResourcePage'; 8 | 9 | jest.mock('react-router-dom', () => { 10 | return { 11 | ...jest.requireActual('react-router-dom'), 12 | useParams: () => ({ 13 | group: 'test-group', 14 | namespace: 'Applications.Core', 15 | type: 'applications', 16 | name: 'test-app', 17 | }), 18 | }; 19 | }); 20 | 21 | describe('ResourcePage', () => { 22 | it('should display loading indicator while loading', async () => { 23 | // This is the boilerplate for an unresolved promise. 24 | const deferred: ((resolve: Resource) => void)[] = []; 25 | const api: Pick = { 26 | getResourceById: async () => 27 | new Promise>(resolve => { 28 | deferred.push( 29 | resolve as unknown as (resolve: Resource) => void, 30 | ); 31 | }), 32 | }; 33 | 34 | await renderInTestApp( 35 | 36 | 37 | , 38 | ); 39 | 40 | await waitFor(() => { 41 | expect(screen.getByTestId('progress')).toBeInTheDocument(); 42 | }); 43 | 44 | // "Complete" the loading of resources. 45 | deferred[0]({ 46 | id: '/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/applications/test-app', 47 | type: 'Applications.Core/applications', 48 | name: 'test-app', 49 | systemData: {}, 50 | properties: {}, 51 | }); 52 | 53 | await waitFor(() => { 54 | expect(screen.queryByTestId('progress')).toBeNull(); 55 | }); 56 | 57 | expect( 58 | screen.getByText( 59 | 'Displaying details for Applications.Core/applications: test-app', 60 | ), 61 | ).toBeInTheDocument(); 62 | 63 | await waitFor(() => { 64 | expect(screen.getByText('Overview')).toBeInTheDocument(); 65 | }); 66 | }); 67 | 68 | it('should display error message when loading fails', async () => { 69 | const api: Pick = { 70 | getResourceById: async () => Promise.reject(new Error('Oh noes!')), 71 | }; 72 | 73 | await renderInTestApp( 74 | 75 | 76 | , 77 | ); 78 | const alert = screen.getByRole('alert'); 79 | expect(alert).toBeInTheDocument(); 80 | expect(alert).toHaveTextContent('Oh noes!'); 81 | }); 82 | 83 | it('should render when loaded', async () => { 84 | const api: Pick = { 85 | getResourceById: async () => 86 | Promise.resolve({ 87 | id: '/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/applications/test-app', 88 | type: 'Applications.Core/applications', 89 | name: 'test-app', 90 | systemData: {}, 91 | properties: {} as T, 92 | }), 93 | }; 94 | 95 | await renderInTestApp( 96 | 97 | 98 | , 99 | ); 100 | 101 | expect( 102 | screen.getByText( 103 | 'Displaying details for Applications.Core/applications: test-app', 104 | ), 105 | ).toBeInTheDocument(); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resources/ResourcePage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ResourceLayout } from './ResourceLayout'; 3 | import { 4 | Progress, 5 | ResponseErrorPanel, 6 | TabbedLayout, 7 | } from '@backstage/core-components'; 8 | import { OverviewTab } from './OverviewTab'; 9 | import { DetailsTab } from './DetailsTab'; 10 | import { EnvironmentProperties, Resource } from '../../resources'; 11 | import useAsync from 'react-use/lib/useAsync'; 12 | import { useApi, useRouteRefParams } from '@backstage/core-plugin-api'; 13 | import { resourcePageRouteRef } from '../../routes'; 14 | import { ApplicationTab } from './ApplicationTab'; 15 | import { ApplicationResourcesTab } from './ApplicationResourcesTab'; 16 | import { radiusApiRef } from '../../plugin'; 17 | import { RecipesTab } from './RecipesTab'; 18 | import { EnvironmentResourcesTab } from './EnvironmentResourcesTab'; 19 | 20 | export const ResourcePage = () => { 21 | const radiusApi = useApi(radiusApiRef); 22 | const params = useRouteRefParams(resourcePageRouteRef); 23 | const id = `/planes/radius/local/resourceGroups/${params.group}/providers/${params.namespace}/${params.type}/${params.name}`; 24 | 25 | const { value, loading, error } = useAsync(async (): Promise => { 26 | return radiusApi.getResourceById({ id }); 27 | }, [id]); 28 | 29 | if (loading) { 30 | return ; 31 | } else if (error) { 32 | return ; 33 | } else if (!value) { 34 | throw new Error('This should not happen.'); 35 | } 36 | 37 | const hasApplication = value?.properties?.application || false; 38 | const isApplication = value?.type === 'Applications.Core/applications'; 39 | let application: string | undefined = undefined; 40 | if (hasApplication) { 41 | application = value.properties.application as string; 42 | } else if (isApplication) { 43 | application = value.id; 44 | } 45 | 46 | const isEnvironment = value?.type === 'Applications.Core/environments'; 47 | 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | {application && ( 58 | 59 | 60 | 61 | )} 62 | {isApplication && ( 63 | 64 | 65 | 66 | )} 67 | {isEnvironment && ( 68 | 69 | } 71 | /> 72 | 73 | )} 74 | {isEnvironment && ( 75 | 76 | 77 | 78 | )} 79 | 80 | 81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resources/index.ts: -------------------------------------------------------------------------------- 1 | export { ResourcePage } from './ResourcePage'; 2 | export { ResourceListPage } from './ResourceListPage'; 3 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resourcetable/ResourceTable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Resource, ResourceList } from '../../resources'; 3 | import { 4 | Progress, 5 | ResponseErrorPanel, 6 | Table, 7 | TableColumn, 8 | } from '@backstage/core-components'; 9 | import { 10 | OptionalResourceLink, 11 | ResourceLink, 12 | } from '../resourcelink/ResourceLink'; 13 | import { useApi } from '@backstage/core-plugin-api'; 14 | import useAsync from 'react-use/lib/useAsync'; 15 | import { radiusApiRef } from '../../plugin'; 16 | import { parseResourceId } from '@radapp.io/rad-components'; 17 | 18 | const DataTable = (props: { 19 | resources: Resource[]; 20 | title: string; 21 | filters?: { environment?: string; application?: string }; 22 | resourceType?: string; 23 | }) => { 24 | const columns: TableColumn[] = [ 25 | { 26 | title: 'Name', 27 | type: 'string', 28 | render: row => , 29 | }, 30 | { 31 | title: 'Resource Group', 32 | type: 'string', 33 | render: row => parseResourceId(row.id)?.group, 34 | }, 35 | { title: 'Type', field: 'type', type: 'string' }, 36 | ]; 37 | 38 | // Special case some additional fields by hiding them when they would never have a value. 39 | if (props.resourceType === 'Applications.Core/environments') { 40 | // Nothing to add 41 | } else if (props.resourceType === 'Applications.Core/applications') { 42 | columns.push({ 43 | title: 'Environment', 44 | field: 'properties.environment', 45 | type: 'string', 46 | render: row => ( 47 | 48 | ), 49 | }); 50 | } else { 51 | columns.push({ 52 | title: 'Application', 53 | field: 'properties.application', 54 | type: 'string', 55 | render: row => ( 56 | 57 | ), 58 | }); 59 | columns.push({ 60 | title: 'Environment', 61 | field: 'properties.environment', 62 | type: 'string', 63 | render: row => ( 64 | 65 | ), 66 | }); 67 | } 68 | 69 | columns.push({ title: 'Status', field: 'properties.provisioningState' }); 70 | 71 | const data = props.resources.filter(resource => { 72 | // If the id equals the filter, then exclude it. 'resource.id' will always be a string. 73 | if ( 74 | props.filters?.environment?.toLowerCase() === resource.id.toLowerCase() 75 | ) { 76 | return false; 77 | } else if ( 78 | props.filters?.application?.toLowerCase() === resource.id.toLowerCase() 79 | ) { 80 | return false; 81 | } 82 | 83 | const application = resource.properties?.application as string; 84 | const environment = resource.properties?.environment as string; 85 | if ( 86 | props.filters?.environment && 87 | environment?.toLowerCase() !== props.filters.environment.toLowerCase() 88 | ) { 89 | return false; 90 | } 91 | if ( 92 | props.filters?.application && 93 | application?.toLowerCase() !== props.filters.application.toLowerCase() 94 | ) { 95 | return false; 96 | } 97 | 98 | return true; 99 | }); 100 | 101 | return ( 102 |
108 | ); 109 | }; 110 | 111 | export const ResourceTable = (props: { 112 | title: string; 113 | resourceType?: string; 114 | filters?: { environment?: string; application?: string }; 115 | }) => { 116 | const radiusApi = useApi(radiusApiRef); 117 | const { value, loading, error } = useAsync( 118 | async (): Promise> => { 119 | return radiusApi.listResources<{ [key: string]: unknown }>({ 120 | resourceType: props.resourceType, 121 | }); 122 | }, 123 | ); 124 | 125 | if (loading) { 126 | return ; 127 | } else if (error) { 128 | return ; 129 | } 130 | 131 | return ( 132 | 138 | ); 139 | }; 140 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/components/resourcetable/index.ts: -------------------------------------------------------------------------------- 1 | export { ResourceTable } from './ResourceTable'; 2 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/features.ts: -------------------------------------------------------------------------------- 1 | export const featureRadiusCatalog = 'radius-catalog'; 2 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/index.ts: -------------------------------------------------------------------------------- 1 | export { featureRadiusCatalog } from './features'; 2 | export { 3 | ApplicationListPage, 4 | EnvironmentListPage, 5 | radiusPlugin, 6 | RecipeListPage, 7 | ResourceListPage, 8 | ResourcePage, 9 | } from './plugin'; 10 | export { 11 | applicationListPageRouteRef, 12 | environmentListPageRouteRef, 13 | recipeListPageRouteRef, 14 | resourceListPageRouteRef, 15 | resourcePageRouteRef, 16 | } from './routes'; 17 | export { RadiusLogo } from './components/logo/RadiusLogo'; 18 | export { RadiusLogomarkReverse } from './components/logo/RadiusLogomarkReverse'; 19 | export { ApplicationIcon } from './components/applications/ApplicationIcon'; 20 | export { EnvironmentIcon } from './components/environments/EnvironmentIcon'; 21 | export { ResourceIcon } from './components/resources/ResourceIcon'; 22 | export { RecipeIcon } from './components/recipes/RecipeIcon'; 23 | export { ApplicationListInfoCard } from './components/applications/ApplicationListInfoCard'; 24 | export { EnvironmentListInfoCard } from './components/environments/EnvironmentListInfoCard'; 25 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/plugin.test.ts: -------------------------------------------------------------------------------- 1 | import { radiusPlugin } from './plugin'; 2 | 3 | describe('radius', () => { 4 | it('should export plugin', () => { 5 | expect(radiusPlugin).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createApiFactory, 3 | createApiRef, 4 | createPlugin, 5 | createRoutableExtension, 6 | } from '@backstage/core-plugin-api'; 7 | 8 | import { 9 | applicationListPageRouteRef, 10 | environmentListPageRouteRef, 11 | recipeListPageRouteRef, 12 | resourceListPageRouteRef, 13 | resourcePageRouteRef, 14 | rootRouteRef, 15 | } from './routes'; 16 | import { RadiusApi } from './api'; 17 | import { KubernetesApi, kubernetesApiRef } from '@backstage/plugin-kubernetes'; 18 | import { RadiusApiImpl } from './api/api'; 19 | import { featureRadiusCatalog as featureRadiusCatalog } from './features'; 20 | 21 | export const radiusApiRef = createApiRef({ 22 | id: 'radius-api', 23 | }); 24 | 25 | export const radiusPlugin = createPlugin({ 26 | id: 'radius', 27 | apis: [ 28 | createApiFactory({ 29 | api: radiusApiRef, 30 | deps: { 31 | kubernetesApi: kubernetesApiRef, 32 | }, 33 | factory: (deps: { kubernetesApi: KubernetesApi }) => { 34 | return new RadiusApiImpl(deps.kubernetesApi); 35 | }, 36 | }), 37 | ], 38 | featureFlags: [ 39 | { 40 | name: featureRadiusCatalog, 41 | }, 42 | ], 43 | routes: { 44 | root: rootRouteRef, 45 | }, 46 | }); 47 | 48 | export const EnvironmentListPage = radiusPlugin.provide( 49 | createRoutableExtension({ 50 | name: 'Environments', 51 | component: () => 52 | import('./components/environments').then(m => m.EnvironmentListPage), 53 | mountPoint: environmentListPageRouteRef, 54 | }), 55 | ); 56 | 57 | export const ApplicationListPage = radiusPlugin.provide( 58 | createRoutableExtension({ 59 | name: 'Applications', 60 | component: () => 61 | import('./components/applications').then(m => m.ApplicationListPage), 62 | mountPoint: applicationListPageRouteRef, 63 | }), 64 | ); 65 | 66 | export const RecipeListPage = radiusPlugin.provide( 67 | createRoutableExtension({ 68 | name: 'recipes', 69 | component: () => import('./components/recipes').then(m => m.RecipeListPage), 70 | mountPoint: recipeListPageRouteRef, 71 | }), 72 | ); 73 | 74 | export const ResourceListPage = radiusPlugin.provide( 75 | createRoutableExtension({ 76 | name: 'Resources', 77 | component: () => 78 | import('./components/resources').then(m => m.ResourceListPage), 79 | mountPoint: resourceListPageRouteRef, 80 | }), 81 | ); 82 | 83 | export const ResourcePage = radiusPlugin.provide( 84 | createRoutableExtension({ 85 | name: 'Resources', 86 | component: () => import('./components/resources').then(m => m.ResourcePage), 87 | mountPoint: resourcePageRouteRef, 88 | }), 89 | ); 90 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/resources/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | Resource, 3 | ResourceList, 4 | ApplicationProperties, 5 | EnvironmentProperties, 6 | } from './resource'; 7 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/resources/resource.ts: -------------------------------------------------------------------------------- 1 | export interface Resource { 2 | id: string; 3 | type: string; 4 | name: string; 5 | tags?: Record; 6 | systemData: Record; 7 | properties: T; 8 | } 9 | 10 | export interface ResourceList { 11 | value: Resource[]; 12 | } 13 | 14 | export interface ApplicationProperties { 15 | provisioningState: string; 16 | environment: string; 17 | } 18 | 19 | export interface Recipe { 20 | templateKind: string; 21 | templatePath: string; 22 | } 23 | 24 | export interface EnvironmentProperties { 25 | provisioningState: string; 26 | recipes: Record>; 27 | } 28 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/resources/resourceId.test.ts: -------------------------------------------------------------------------------- 1 | import { parseResourceId } from '@radapp.io/rad-components'; 2 | 3 | describe('parseResourceId', () => { 4 | it('should parse a valid environment resource ID', () => { 5 | const resourceId = 6 | '/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/test-environment'; 7 | const parsed = parseResourceId(resourceId); 8 | expect(parsed).toEqual({ 9 | plane: 'local', 10 | group: 'test-group', 11 | type: 'Applications.Core/environments', 12 | name: 'test-environment', 13 | }); 14 | }); 15 | 16 | it('should return undefined for an invalid resource ID', () => { 17 | const resourceId = 18 | '/planes/radius/local/resourceGroups/test-group/providers/Applications.Cor12323231e-----/environments'; 19 | const parsed = parseResourceId(resourceId); 20 | expect(parsed).toBeUndefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/resources/resourceId.ts: -------------------------------------------------------------------------------- 1 | const RESOURCE_ID_REGEX = 2 | /^\/planes\/radius\/(?[0-9a-zA-Z-]+)\/resourceGroups\/(?[0-9a-zA-Z-]+)\/providers\/(?[a-zA-Z\\.]+)\/(?[a-zA-Z]+)\/(?[0-9a-zA-Z-]+)$/i; 3 | 4 | export interface ResourceId { 5 | plane: string; 6 | group: string; 7 | type: string; 8 | name: string; 9 | } 10 | 11 | export function parseResourceId(resourceId: string): ResourceId | undefined { 12 | const match = RESOURCE_ID_REGEX.exec(resourceId); 13 | if (!match) { 14 | return undefined; 15 | } 16 | 17 | const { plane, group, namespace, type, name } = match.groups ?? {}; 18 | return { plane, group, type: `${namespace}/${type}`, name }; 19 | } 20 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/routes.ts: -------------------------------------------------------------------------------- 1 | import { createRouteRef } from '@backstage/core-plugin-api'; 2 | 3 | export const rootRouteRef = createRouteRef({ 4 | id: 'radius', 5 | }); 6 | 7 | export const applicationListPageRouteRef = createRouteRef({ 8 | id: 'radius-application-list-page', 9 | }); 10 | 11 | export const environmentListPageRouteRef = createRouteRef({ 12 | id: 'radius-environment-list-page', 13 | }); 14 | 15 | export const recipeListPageRouteRef = createRouteRef({ 16 | id: 'radius-recipe-list-page', 17 | }); 18 | 19 | export const resourceListPageRouteRef = createRouteRef({ 20 | id: 'radius-resource-list-page', 21 | }); 22 | 23 | export const resourcePageRouteRef = createRouteRef({ 24 | id: 'radius-resource-page', 25 | params: ['group', 'namespace', 'type', 'name'], 26 | }); 27 | -------------------------------------------------------------------------------- /plugins/plugin-radius/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import 'jest-canvas-mock'; 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // Global settings for the project go here. 3 | "extends": "./node_modules/@backstage/cli/config/tsconfig.json", 4 | "include": ["packages/*/src", "plugins/*/src"], 5 | "exclude": [ 6 | "dist/", 7 | "dist-types/", 8 | "node_modules/", 9 | "packages/*/**/__docs__/" 10 | ], 11 | "compilerOptions": { 12 | "outDir": "dist-types", 13 | "rootDir": "." 14 | } 15 | } 16 | --------------------------------------------------------------------------------