plugin(redhat-argocd): migrate of Janus argocd plugins to backstage/community-plugins repository (#856)
* initial redhat-argocd workspace creation * add missing dependency react-router-dom * change skipLibCheck to true * remove --tsc form api reports * override api report * add missing devDependencies, react-dom and react * Run prettier * update names * update Changeset/cuddly-mice-jam.md file with a message * regenerate API reports * update pluginPackage name * update yarn.lock to match package.json * update links --------- Signed-off-by: Fortune-Ndlovu <fndlovu@redhat.com> Signed-off-by: Bethany Griggs <bethanyngriggs@gmail.com> Co-authored-by: Beth Griggs <bethanyngriggs@gmail.com>
This commit is contained in:
parent
c81314e4c2
commit
3b45ff62e1
|
|
@ -0,0 +1,8 @@
|
|||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch"
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
'@backstage-community/plugin-redhat-argocd-common': patch
|
||||
'@backstage-community/plugin-redhat-argocd': patch
|
||||
---
|
||||
|
||||
The `argocd` and `argocd-common` plugins from the [janus-idp/backstage-plugins](https://github.com/janus-idp/backstage-plugins) repository were migrated to the community plugins, based on commit [c3232099](https://github.com/janus-idp/backstage-plugins/commit/c3232099). The migration was performed by following the manual migration steps outlined in the [Community Plugins CONTRIBUTING guide](https://github.com/backstage/community-plugins/blob/main/CONTRIBUTING.md#migrating-a-plugin)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
.git
|
||||
.yarn/cache
|
||||
.yarn/install-state.gz
|
||||
node_modules
|
||||
packages/*/src
|
||||
packages/*/node_modules
|
||||
plugins
|
||||
*.local.yaml
|
||||
|
|
@ -0,0 +1 @@
|
|||
playwright.config.ts
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
};
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Coverage directory generated when running tests with coverage
|
||||
coverage
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Yarn 3 files
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# Node version directives
|
||||
.nvmrc
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# Build output
|
||||
dist
|
||||
dist-types
|
||||
|
||||
# Temporary change files created by Vim
|
||||
*.swp
|
||||
|
||||
# MkDocs build output
|
||||
site
|
||||
|
||||
# Local configuration files
|
||||
*.local.yaml
|
||||
|
||||
# Sensitive credentials
|
||||
*-credentials.yaml
|
||||
|
||||
# vscode database functionality support files
|
||||
*.session.sql
|
||||
|
||||
# E2E test reports
|
||||
e2e-test-report/
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
dist
|
||||
dist-types
|
||||
coverage
|
||||
.vscode
|
||||
.eslintrc.js
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# [Backstage](https://backstage.io)
|
||||
|
||||
This is your newly scaffolded Backstage App, Good Luck!
|
||||
|
||||
To start the app, run:
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
yarn dev
|
||||
```
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"version": "1.29.0"
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: redhat-argocd
|
||||
description: An example of a Backstage application.
|
||||
# Example for optional annotations
|
||||
# annotations:
|
||||
# github.com/project-slug: backstage/backstage
|
||||
# backstage.io/techdocs-ref: dir:.
|
||||
spec:
|
||||
type: website
|
||||
owner: john@example.com
|
||||
lifecycle: experimental
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"name": "@internal/redhat-argocd",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "18 || 20"
|
||||
},
|
||||
"scripts": {
|
||||
"tsc": "tsc",
|
||||
"tsc:full": "tsc --skipLibCheck true --incremental false",
|
||||
"build:all": "backstage-cli repo build --all",
|
||||
"build:api-reports": "yarn build:api-reports:only",
|
||||
"build:api-reports:only": "backstage-repo-tools api-reports --allow-all-warnings -o ae-wrong-input-file-type --validate-release-tags",
|
||||
"clean": "backstage-cli repo clean",
|
||||
"test": "backstage-cli repo test",
|
||||
"test:all": "backstage-cli repo test --coverage",
|
||||
"fix": "backstage-cli repo fix",
|
||||
"lint": "backstage-cli repo lint --since origin/main",
|
||||
"lint:all": "backstage-cli repo lint",
|
||||
"prettier:check": "prettier --check .",
|
||||
"new": "backstage-cli new --scope @backstage-community"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/*",
|
||||
"plugins/*"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/community-plugins",
|
||||
"directory": "workspaces/redhat-argocd"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.26.11",
|
||||
"@backstage/e2e-test-utils": "^0.1.1",
|
||||
"@backstage/repo-tools": "^0.8.0",
|
||||
"@changesets/cli": "^2.27.1",
|
||||
"@spotify/prettier-config": "^12.0.0",
|
||||
"node-gyp": "^9.0.0",
|
||||
"prettier": "^2.3.2",
|
||||
"typescript": "~5.3.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@microsoft/api-extractor": "7.36.4"
|
||||
},
|
||||
"prettier": "@spotify/prettier-config",
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,mjs,cjs}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{json,md}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# The Plugins Folder
|
||||
|
||||
This is where your own plugins and their associated modules live, each in a
|
||||
separate folder of its own.
|
||||
|
||||
If you want to create a new plugin here, go to your project root directory, run
|
||||
the command `yarn new`, and follow the on-screen instructions.
|
||||
|
||||
You can also check out existing plugins on [the plugin marketplace](https://backstage.io/plugins)!
|
||||
|
|
@ -0,0 +1 @@
|
|||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# argocd-common
|
||||
|
||||
Welcome to the argocd-common plugin!
|
||||
|
||||
This plugin contains common utilities for the argocd plugin.
|
||||
|
||||
# Argo CD plugin for Backstage
|
||||
|
||||
The Argocd plugin displays the information about your argocd applications in your Backstage application.
|
||||
|
||||
For more information about Argocd plugin, see the [Argocd plugin documentation](https://github.com/backstage/community-plugins/tree/main/workspaces/redhat-argocd) on GitHub.
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
## API Report File for "@backstage-community/plugin-redhat-argocd-common"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { BasicPermission } from '@backstage/plugin-permission-common';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "argocdPermissions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export const argocdPermissions: BasicPermission[];
|
||||
|
||||
// Warning: (ae-missing-release-tag) "argocdViewPermission" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const argocdViewPermission: BasicPermission;
|
||||
```
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"name": "@backstage-community/plugin-redhat-argocd-common",
|
||||
"version": "1.0.1",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "common-library",
|
||||
"supported-versions": "1.28.4",
|
||||
"pluginId": "redhat-argocd-common",
|
||||
"pluginPackage": "@backstage-community/plugin-redhat-argocd-common",
|
||||
"pluginPackages": [
|
||||
"@backstage-community/plugin-redhat-argocd-common"
|
||||
]
|
||||
},
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"lint": "backstage-cli package lint",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"test": "backstage-cli package test --passWithNoTests --coverage",
|
||||
"tsc": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/plugin-permission-common": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "0.26.11"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/backstage/community-plugins.git",
|
||||
"directory": "workspaces/redhat-argocd/plugins/argocd-common"
|
||||
},
|
||||
"keywords": [
|
||||
"support:production",
|
||||
"lifecycle:active",
|
||||
"backstage",
|
||||
"plugin"
|
||||
],
|
||||
"homepage": "https://red.ht/rhdh",
|
||||
"bugs": "https://github.com/backstage/community-plugins/issues",
|
||||
"maintainers": [
|
||||
"karthikjeeyar",
|
||||
"rohitkrai03",
|
||||
"Eswaraiahsapram"
|
||||
],
|
||||
"author": "Red Hat"
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Common functionalities for the argocd plugin.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export * from './permissions';
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
|
||||
export const argocdViewPermission = createPermission({
|
||||
name: 'argocd.view.read',
|
||||
attributes: {
|
||||
action: 'read',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* List of all permissions on permission polices.
|
||||
*/
|
||||
export const argocdPermissions = [argocdViewPermission];
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "@backstage/cli/config/tsconfig.json",
|
||||
"include": ["src", "dev"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist-types/plugins/argocd-common",
|
||||
"rootDir": "."
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": ["//"],
|
||||
"pipeline": {
|
||||
"tsc": {
|
||||
"outputs": ["../../dist-types/plugins/argocd-common/**"],
|
||||
"dependsOn": ["^tsc"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
## @janus-idp/backstage-plugin-argocd [1.5.7](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.5.6...@janus-idp/backstage-plugin-argocd@1.5.7) (2024-08-02)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **@janus-idp/backstage-plugin-argocd-common:** upgraded to 1.0.0
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.5.6](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.5.5...@janus-idp/backstage-plugin-argocd@1.5.6) (2024-08-02)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **@janus-idp/backstage-plugin-argocd-common:** upgraded to 1.0.0
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.5.5](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.5.4...@janus-idp/backstage-plugin-argocd@1.5.5) (2024-08-01)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **@janus-idp/backstage-plugin-argocd-common:** upgraded to 1.0.0
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.5.4](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.5.3...@janus-idp/backstage-plugin-argocd@1.5.4) (2024-07-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- argocd dependency package version ([#1992](https://github.com/janus-idp/backstage-plugins/issues/1992)) ([e3c4419](https://github.com/janus-idp/backstage-plugins/commit/e3c4419318ea3a24f8e6369decfadd26be10ae00))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **@janus-idp/backstage-plugin-argocd-common:** upgraded to 1.0.0
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.5.3](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.5.2...@janus-idp/backstage-plugin-argocd@1.5.3) (2024-07-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **argocd:** fix argocd naming ([#1990](https://github.com/janus-idp/backstage-plugins/issues/1990)) ([6b764a8](https://github.com/janus-idp/backstage-plugins/commit/6b764a8105811475c1d71ea2f78077d1b6b6e6d8))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **@janus-idp/backstage-plugin-argocd-common:** upgraded to 1.0.0
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.5.2](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.5.1...@janus-idp/backstage-plugin-argocd@1.5.2) (2024-07-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **argocd:** fix argocd-common plugin version ([#1987](https://github.com/janus-idp/backstage-plugins/issues/1987)) ([fb441fe](https://github.com/janus-idp/backstage-plugins/commit/fb441fe637137da08dc7388dbd75e58c775e01ea))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **@janus-idp/backstage-plugin-argocd-common:** upgraded to 1.0.0
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.5.1](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.5.0...@janus-idp/backstage-plugin-argocd@1.5.1) (2024-07-31)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **@janus-idp/backstage-plugin-argocd-common:** upgraded to 1.0.0
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.5.0](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.4.1...@janus-idp/backstage-plugin-argocd@1.5.0) (2024-07-30)
|
||||
|
||||
### Features
|
||||
|
||||
- **argocd:** add permission support for argocd ([#1855](https://github.com/janus-idp/backstage-plugins/issues/1855)) ([3b78237](https://github.com/janus-idp/backstage-plugins/commit/3b782377683605ea4d584c43bea14be2f435003d))
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.4.1](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.4.0...@janus-idp/backstage-plugin-argocd@1.4.1) (2024-07-26)
|
||||
|
||||
### Documentation
|
||||
|
||||
- **argocd:** update argocd configuration documentation ([#1875](https://github.com/janus-idp/backstage-plugins/issues/1875)) ([054ceec](https://github.com/janus-idp/backstage-plugins/commit/054ceec2bdca47e1ee251b2d882671b2c95915c6))
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.4.0](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.3.0...@janus-idp/backstage-plugin-argocd@1.4.0) (2024-07-25)
|
||||
|
||||
### Features
|
||||
|
||||
- **deps:** update to backstage 1.29 ([#1900](https://github.com/janus-idp/backstage-plugins/issues/1900)) ([f53677f](https://github.com/janus-idp/backstage-plugins/commit/f53677fb02d6df43a9de98c43a9f101a6db76802))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** update dependency react-use to v17.5.1 ([#1943](https://github.com/janus-idp/backstage-plugins/issues/1943)) ([0c05ad5](https://github.com/janus-idp/backstage-plugins/commit/0c05ad5cc1aef3df1d14f1ffa59933850a04ebbc))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **@janus-idp/cli:** upgraded to 1.13.0
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.3.0](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.2.4...@janus-idp/backstage-plugin-argocd@1.3.0) (2024-07-23)
|
||||
|
||||
### Features
|
||||
|
||||
- **deps:** update to backstage 1.28 ([#1891](https://github.com/janus-idp/backstage-plugins/issues/1891)) ([1ba1108](https://github.com/janus-idp/backstage-plugins/commit/1ba11088e0de60e90d138944267b83600dc446e5))
|
||||
|
||||
### Documentation
|
||||
|
||||
- fix argocd naming ([#1904](https://github.com/janus-idp/backstage-plugins/issues/1904)) ([3173d79](https://github.com/janus-idp/backstage-plugins/commit/3173d79b9b226fd994d48f27b392df7d167d4e64))
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.2.4](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.2.3...@janus-idp/backstage-plugin-argocd@1.2.4) (2024-07-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **argocd:** fix argocd commit message visibility ([#1874](https://github.com/janus-idp/backstage-plugins/issues/1874)) ([e558d75](https://github.com/janus-idp/backstage-plugins/commit/e558d7549c49a1821eafc9424f174c6d457ce414))
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.2.3](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.2.2...@janus-idp/backstage-plugin-argocd@1.2.3) (2024-06-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **argocd:** hide commit section for helm based applications ([#1834](https://github.com/janus-idp/backstage-plugins/issues/1834)) ([a86ce1e](https://github.com/janus-idp/backstage-plugins/commit/a86ce1e5237ac419eb93a9766cb8e2736ba9b8d7))
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.2.2](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.2.1...@janus-idp/backstage-plugin-argocd@1.2.2) (2024-06-19)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **@janus-idp/cli:** upgraded to 1.11.1
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.2.1](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.2.0...@janus-idp/backstage-plugin-argocd@1.2.1) (2024-06-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** update dependency react-use to v17.5.0 ([#1780](https://github.com/janus-idp/backstage-plugins/issues/1780)) ([a25bf15](https://github.com/janus-idp/backstage-plugins/commit/a25bf15e14e9fa5a946551c5626c92f6b1f83d2f))
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.2.0](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.1.10...@janus-idp/backstage-plugin-argocd@1.2.0) (2024-06-13)
|
||||
|
||||
### Features
|
||||
|
||||
- **deps:** update to backstage 1.27 ([#1683](https://github.com/janus-idp/backstage-plugins/issues/1683)) ([a14869c](https://github.com/janus-idp/backstage-plugins/commit/a14869c3f4177049cb8d6552b36c3ffd17e7997d))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **@janus-idp/cli:** upgraded to 1.11.0
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.1.10](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.1.9...@janus-idp/backstage-plugin-argocd@1.1.10) (2024-06-13)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **@janus-idp/cli:** upgraded to 1.10.1
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.1.9](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.1.8...@janus-idp/backstage-plugin-argocd@1.1.9) (2024-06-10)
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.1.8](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.1.7...@janus-idp/backstage-plugin-argocd@1.1.8) (2024-06-05)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **@janus-idp/cli:** upgraded to 1.10.0
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.1.7](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.1.6...@janus-idp/backstage-plugin-argocd@1.1.7) (2024-06-04)
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.1.6](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.1.5...@janus-idp/backstage-plugin-argocd@1.1.6) (2024-06-03)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **@janus-idp/cli:** upgraded to 1.9.0
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.1.5](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.1.4...@janus-idp/backstage-plugin-argocd@1.1.5) (2024-05-31)
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.1.4](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.1.3...@janus-idp/backstage-plugin-argocd@1.1.4) (2024-05-29)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **@janus-idp/cli:** upgraded to 1.8.10
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.1.3](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.1.2...@janus-idp/backstage-plugin-argocd@1.1.3) (2024-05-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **release:** change problematic plugins to private ([#1738](https://github.com/janus-idp/backstage-plugins/issues/1738)) ([69176bd](https://github.com/janus-idp/backstage-plugins/commit/69176bd75ccd842a313445e096223ecc339b655b))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **@janus-idp/cli:** upgraded to 1.8.9
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.1.2](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.1.1...@janus-idp/backstage-plugin-argocd@1.1.2) (2024-05-28)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **@janus-idp/cli:** upgraded to 1.8.8
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.1.1](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.1.0...@janus-idp/backstage-plugin-argocd@1.1.1) (2024-05-16)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **@janus-idp/cli:** upgraded to 1.8.7
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.1.0](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.0.4...@janus-idp/backstage-plugin-argocd@1.1.0) (2024-05-14)
|
||||
|
||||
### Features
|
||||
|
||||
- **deps:** use RHDH themes in the backstage app and dev pages ([#1480](https://github.com/janus-idp/backstage-plugins/issues/1480)) ([8263bf0](https://github.com/janus-idp/backstage-plugins/commit/8263bf099736cbb0d0f2316082d338ba81fa6927))
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.0.4](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.0.3...@janus-idp/backstage-plugin-argocd@1.0.4) (2024-05-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **argocd:** make refreshInterval configuration as optional ([#1647](https://github.com/janus-idp/backstage-plugins/issues/1647)) ([2c24d35](https://github.com/janus-idp/backstage-plugins/commit/2c24d35f050801801c597967e890b6d2e647fb06))
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.0.3](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.0.2...@janus-idp/backstage-plugin-argocd@1.0.3) (2024-05-09)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **@janus-idp/cli:** upgraded to 1.8.6
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.0.2](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.0.1...@janus-idp/backstage-plugin-argocd@1.0.2) (2024-05-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **argocd:** fix sonarcloud lint warnings in argocd plugin ([#1620](https://github.com/janus-idp/backstage-plugins/issues/1620)) ([66d3763](https://github.com/janus-idp/backstage-plugins/commit/66d3763324d83875fa30d568cd3fd1d69c72a7e7))
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd [1.0.1](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.0.0...@janus-idp/backstage-plugin-argocd@1.0.1) (2024-05-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **argocd:** fix argocd configurations visibility ([#1618](https://github.com/janus-idp/backstage-plugins/issues/1618)) ([ade677f](https://github.com/janus-idp/backstage-plugins/commit/ade677f1bccff30b16091c76112c3b6aaf7fa421))
|
||||
|
||||
### Other changes
|
||||
|
||||
- **argocd:** add playwright dev mode tests ([#1616](https://github.com/janus-idp/backstage-plugins/issues/1616)) ([07c1452](https://github.com/janus-idp/backstage-plugins/commit/07c1452b3098f2a4a59477845a9ab678d1766fba))
|
||||
|
||||
## @janus-idp/backstage-plugin-argocd 1.0.0 (2024-05-07)
|
||||
|
||||
### Features
|
||||
|
||||
- **argocd:** add argocd deployment lifecycle and summary component ([#1540](https://github.com/janus-idp/backstage-plugins/issues/1540)) ([4c7c533](https://github.com/janus-idp/backstage-plugins/commit/4c7c533cae664efc5deff15f7411ed4d74c287a7))
|
||||
- **argocd:** create a new plugin for argocd ([#1360](https://github.com/janus-idp/backstage-plugins/issues/1360)) ([a3b6916](https://github.com/janus-idp/backstage-plugins/commit/a3b691688942c53892717f8f05e0e06bdaba6454))
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
# Argo CD plugin for Backstage
|
||||
|
||||
## Getting started
|
||||
|
||||
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 [/argocd/deployment-lifecycle](http://localhost:3000/argocd/deployment-lifecycle).
|
||||
|
||||
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
|
||||
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
|
||||
It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
|
||||
|
||||
## For Administrators
|
||||
|
||||
### Installation and configuration
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- Install `@roadiehq/backstage-plugin-argo-cd-backend` plugin using the following command from the root directory
|
||||
<!-- configure it by following [Argo CD Backend Plugin docs](https://www.npmjs.com/package/@roadiehq/backstage-plugin-argo-cd-backend) -->
|
||||
|
||||
```bash
|
||||
yarn workspace app add @roadiehq/backstage-plugin-argo-cd-backend
|
||||
```
|
||||
|
||||
- Create plugin file for Argo CD backend in your `packages/backend/src/plugins/` directory.
|
||||
|
||||
```ts
|
||||
// packages/backend/src/plugins/argocd.ts
|
||||
|
||||
import { createRouter } from '@roadiehq/backstage-plugin-argo-cd-backend';
|
||||
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
config,
|
||||
}: PluginEnvironment) {
|
||||
return await createRouter({ logger, config });
|
||||
}
|
||||
```
|
||||
|
||||
- Modify your backend router to expose the APIs for Argo CD backend
|
||||
|
||||
```ts
|
||||
// packages/backend/src/index.ts
|
||||
|
||||
import {legacyPlugin} from '@backstage/backend-common';
|
||||
...
|
||||
|
||||
backend.add(legacyPlugin('argocd', import('./plugins/argocd')));
|
||||
```
|
||||
|
||||
- add argocd instance information in app.config.yaml
|
||||
|
||||
```ts
|
||||
argocd:
|
||||
appLocatorMethods:
|
||||
- type: 'config'
|
||||
instances:
|
||||
- name: argoInstance1
|
||||
url: https://argoInstance1.com
|
||||
username: ${ARGOCD_USERNAME}
|
||||
password: ${ARGOCD_PASSWORD}
|
||||
- name: argoInstance2
|
||||
url: https://argoInstance2.com
|
||||
username: ${ARGOCD_USERNAME}
|
||||
password: ${ARGOCD_PASSWORD}
|
||||
```
|
||||
|
||||
#### How to add argocd frontend plugin to Backstage app
|
||||
|
||||
1. Install the Argocd plugin using the following command:
|
||||
|
||||
```bash
|
||||
yarn workspace app add @janus-idp/backstage-plugin-argocd
|
||||
```
|
||||
|
||||
2. Add deployment summary and deployment lifecycle compoennt to the `entityPage.tsx` source file:
|
||||
|
||||
```ts
|
||||
// packages/app/src/components/catalog/EntityPage.tsx
|
||||
import {
|
||||
ArgocdDeploymentSummary,
|
||||
ArgocdDeploymentLifecycle,
|
||||
isArgocdConfigured,
|
||||
} from '@janus-idp/backstage-plugin-argocd';
|
||||
|
||||
const overviewContent = (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
...
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={e => Boolean(isArgocdConfigured(e))}>
|
||||
<Grid item sm={12}>
|
||||
<ArgocdDeploymentSummary />
|
||||
</Grid>
|
||||
</EntitySwitch.Case>
|
||||
</EntitySwitch>
|
||||
...
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const cicdcontent = (
|
||||
<EntitySwitch>
|
||||
{/* ... */}
|
||||
{/* highlight-add-start */}
|
||||
...
|
||||
<EntitySwitch.Case if={e => Boolean(isArgocdConfigured(e))}>
|
||||
<Grid item sm={12}>
|
||||
<ArgocdDeploymentLifecycle />
|
||||
</Grid>
|
||||
</EntitySwitch.Case>
|
||||
{/* highlight-add-end */}
|
||||
</EntitySwitch>
|
||||
);
|
||||
```
|
||||
|
||||
- Add one of the following annotations to the entity's `catalog-info.yaml` file to enable Argo CD features in the backstage instance:
|
||||
|
||||
- To get all the applications matching the metadata labels.
|
||||
|
||||
```yaml
|
||||
annotations:
|
||||
...
|
||||
|
||||
argocd/app-selector: 'rht-gitops.com/janus-argocd=quarkus-app' # format: `label.key=label.value`
|
||||
|
||||
```
|
||||
|
||||
**or**
|
||||
|
||||
- To fetch a single application, use the following annotation in `catalog-info.yaml` file:
|
||||
|
||||
```yaml
|
||||
annotations:
|
||||
...
|
||||
|
||||
argocd/app-name: 'quarkus-app'
|
||||
|
||||
```
|
||||
|
||||
> [!Note] > **You should not add both the annotations in the same catalog, adding both annotations will result in error in the plugin.**
|
||||
|
||||
- To switch between argocd instances, you can use the following annotation
|
||||
|
||||
```yaml
|
||||
annotations:
|
||||
...
|
||||
argocd/instance-name: 'argoInstance2'
|
||||
```
|
||||
|
||||
> [!Note] > **If this annotation is not set, the plugin will default to the first Argo CD instance configured in the `app.config.yaml`**
|
||||
|
||||
## Loading as Dynamic Plugin
|
||||
|
||||
To install this plugin into Red Hat Developer Hub or Janus IDP via Helm use this configuration:
|
||||
|
||||
```yaml
|
||||
global:
|
||||
dynamic:
|
||||
includes:
|
||||
- dynamic-plugins.default.yaml
|
||||
plugins:
|
||||
- package: ./dynamic-plugins/dist/roadiehq-backstage-plugin-argo-cd-backend-dynamic
|
||||
disabled: false
|
||||
- package: ./dynamic-plugins/dist/janus-idp-backstage-plugin-argocd
|
||||
disabled: false
|
||||
```
|
||||
|
||||
This plugin can be loaded in backstage showcase application as a dynamic plugin.
|
||||
|
||||
Follow the below steps -
|
||||
|
||||
- Export dynamic plugin assets. This will build and create the static assets for the plugin and put it inside dist-scalprum folder.
|
||||
|
||||
`yarn install`
|
||||
|
||||
`yarn tsc`
|
||||
|
||||
`yarn build`
|
||||
|
||||
`yarn export-dynamic`
|
||||
|
||||
- Package and copy dist-scalprum folder assets to dynamic-plugins-root folder in showcase application.
|
||||
|
||||
```sh
|
||||
pkg=../plugins/argocd
|
||||
archive=$(npm pack $pkg)
|
||||
tar -xzf "$archive" && rm "$archive"
|
||||
mv package $(echo $archive | sed -e 's:\.tgz$::')
|
||||
```
|
||||
|
||||
- Add the extension point inside the `app-config.yaml` or `app-config.local.yaml` file.
|
||||
|
||||
```yaml
|
||||
dynamicPlugins:
|
||||
frontend:
|
||||
janus-idp.backstage-plugin-argocd:
|
||||
mountPoints:
|
||||
- mountPoint: entity.page.overview/cards
|
||||
importName: ArgocdDeploymentSummary
|
||||
config:
|
||||
layout:
|
||||
gridColumnEnd:
|
||||
lg: 'span 8'
|
||||
xs: 'span 12'
|
||||
if:
|
||||
allOf:
|
||||
- isArgocdAvailable
|
||||
- mountPoint: entity.page.cd/cards
|
||||
importName: ArgocdDeploymentLifecycle
|
||||
config:
|
||||
layout:
|
||||
gridColumn: '1 / -1'
|
||||
if:
|
||||
allOf:
|
||||
- isArgocdConfigured
|
||||
```
|
||||
|
||||
For more detailed explanation on dynamic plugins follow this [doc](https://github.com/janus-idp/backstage-showcase/blob/main/showcase-docs/dynamic-plugins.md).
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
## API Report File for "@backstage-community/plugin-redhat-argocd"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
/// <reference types="react" />
|
||||
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "ArgocdDeploymentLifecycle" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const ArgocdDeploymentLifecycle: () => JSX_2.Element | null;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "ArgocdDeploymentSummary" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const ArgocdDeploymentSummary: () => JSX_2.Element | null;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "argocdPlugin" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const argocdPlugin: BackstagePlugin<
|
||||
{
|
||||
root: RouteRef<undefined>;
|
||||
},
|
||||
{},
|
||||
{}
|
||||
>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "isArgocdConfigured" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const isArgocdConfigured: (entity: Entity) => boolean;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
dynamicPlugins:
|
||||
frontend:
|
||||
janus-idp.backstage-plugin-argocd:
|
||||
mountPoints:
|
||||
- mountPoint: entity.page.cd/cards
|
||||
importName: ArgocdPage
|
||||
config:
|
||||
layout:
|
||||
gridColumn: '1 / -1'
|
||||
if:
|
||||
anyOf:
|
||||
- hasAnnotation: backstage.io/kubernetes-id
|
||||
- hasAnnotation: backstage.io/kubernetes-namespace
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
export interface Config {
|
||||
/** Optional configurations for the ArgoCD plugin */
|
||||
argocd?: {
|
||||
/**
|
||||
* The base url of the ArgoCD instance.
|
||||
* @visibility frontend
|
||||
*/
|
||||
baseUrl?: string;
|
||||
/**
|
||||
* Support for the ArgoCD beta feature "Applications in any namespace"
|
||||
* @visibility frontend
|
||||
*/
|
||||
namespacedApps?: boolean;
|
||||
/**
|
||||
* The number of revisions to load per application in the history table.
|
||||
* @visibility frontend
|
||||
*/
|
||||
revisionsToLoad?: number;
|
||||
/**
|
||||
* Polling interval timeout
|
||||
* @visibility frontend
|
||||
*/
|
||||
refreshInterval?: number;
|
||||
/**
|
||||
* The base url of the ArgoCD instance.
|
||||
* @visibility frontend
|
||||
*/
|
||||
appLocatorMethods?: Array</**
|
||||
* @visibility frontend
|
||||
*/
|
||||
{
|
||||
/**
|
||||
* The base url of the ArgoCD instance.
|
||||
* @visibility frontend
|
||||
*/
|
||||
type: string;
|
||||
instances: Array<{
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* @visibility secret
|
||||
*/
|
||||
username: string;
|
||||
/**
|
||||
* @visibility secret
|
||||
*/
|
||||
password: string;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,296 @@
|
|||
import { Application } from '../../src/types';
|
||||
|
||||
const commonMetadata = {
|
||||
creationTimestamp: new Date('2024-04-22T05:39:23Z'),
|
||||
labels: {
|
||||
'rht-gitops.com/janus-argocd': 'quarkus-app-bootstrap',
|
||||
},
|
||||
instance: { name: 'main' },
|
||||
name: 'quarkus-app-dev',
|
||||
};
|
||||
|
||||
const commonSpec = {
|
||||
project: 'janus',
|
||||
destination: {
|
||||
namespace: 'quarkus-app-dev',
|
||||
server: 'https://kubernetes.default.svc',
|
||||
},
|
||||
source: {
|
||||
helm: {
|
||||
parameters: [
|
||||
{
|
||||
name: 'namespace.name',
|
||||
value: 'quarkus-app-dev',
|
||||
},
|
||||
{
|
||||
name: 'environment',
|
||||
value: 'dev',
|
||||
},
|
||||
{
|
||||
name: 'image.tag',
|
||||
value: 'latest',
|
||||
},
|
||||
],
|
||||
},
|
||||
path: './helm/app',
|
||||
repoURL:
|
||||
'https://gitlab-gitlab.apps.cluster.test.com/development/quarkus-app-gitops.git',
|
||||
targetRevision: 'HEAD',
|
||||
},
|
||||
};
|
||||
|
||||
const commonStatus = {
|
||||
history: [
|
||||
{
|
||||
revision: '90f9758b7033a4bbb7c33a35ee474d61091644bc',
|
||||
deployedAt: '2024-04-22T05:39:24Z',
|
||||
id: 0,
|
||||
source: {
|
||||
repoURL:
|
||||
'https://gitlab-gitlab.apps.cluster.test.com/development/quarkus-app-gitops.git',
|
||||
path: './helm/app',
|
||||
targetRevision: 'HEAD',
|
||||
helm: {
|
||||
parameters: [
|
||||
{
|
||||
name: 'namespace.name',
|
||||
value: 'quarkus-app-dev',
|
||||
},
|
||||
{
|
||||
name: 'environment',
|
||||
value: 'dev',
|
||||
},
|
||||
{
|
||||
name: 'image.tag',
|
||||
value: 'latest',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
deployStartedAt: '2024-04-22T05:39:23Z',
|
||||
},
|
||||
{
|
||||
revision: '90f9758b7033a4bbb7c33a35ee474d61091644bc',
|
||||
deployedAt: '2024-04-22T17:57:40Z',
|
||||
id: 1,
|
||||
source: {
|
||||
repoURL:
|
||||
'https://gitlab-gitlab.apps.cluster.test.com/development/quarkus-app-gitops.git',
|
||||
path: './helm/app',
|
||||
targetRevision: 'HEAD',
|
||||
helm: {
|
||||
parameters: [
|
||||
{
|
||||
name: 'namespace.name',
|
||||
value: 'quarkus-app-dev',
|
||||
},
|
||||
{
|
||||
name: 'environment',
|
||||
value: 'dev',
|
||||
},
|
||||
{
|
||||
name: 'image.tag',
|
||||
value: 'latest',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
deployStartedAt: '2024-04-22T17:57:40Z',
|
||||
},
|
||||
],
|
||||
health: {
|
||||
status: 'Healthy',
|
||||
},
|
||||
operationState: {
|
||||
operation: {
|
||||
sync: {
|
||||
prune: true,
|
||||
revision: '90f9758b7033a4bbb7c33a35ee474d61091644bc',
|
||||
syncOptions: [
|
||||
'RespectIgnoreDifferences=true',
|
||||
'ApplyOutOfSyncOnly=true',
|
||||
],
|
||||
},
|
||||
},
|
||||
phase: 'Succeeded',
|
||||
},
|
||||
summary: {
|
||||
images: ['quay-hw6fw.apps.cluster.test.com/quayadmin/quarkus-app:latest'],
|
||||
},
|
||||
sync: {
|
||||
status: 'Synced',
|
||||
},
|
||||
};
|
||||
|
||||
export const mockApplication: Application = {
|
||||
metadata: {
|
||||
...commonMetadata,
|
||||
creationTimestamp: new Date('2024-04-22T05:39:23Z'),
|
||||
name: 'quarkus-app-dev',
|
||||
},
|
||||
spec: commonSpec,
|
||||
status: commonStatus,
|
||||
};
|
||||
|
||||
const preProdHelmParameters = {
|
||||
parameters: [
|
||||
{
|
||||
name: 'namespace.name',
|
||||
value: 'quarkus-app-preprod',
|
||||
},
|
||||
{
|
||||
name: 'environment',
|
||||
value: 'preprod',
|
||||
},
|
||||
{
|
||||
name: 'image.tag',
|
||||
value: 'latest',
|
||||
},
|
||||
],
|
||||
};
|
||||
export const preProdApplication = {
|
||||
metadata: {
|
||||
...commonMetadata,
|
||||
creationTimestamp: new Date('2024-04-22T05:39:23Z'),
|
||||
name: 'quarkus-app-preprod',
|
||||
},
|
||||
spec: {
|
||||
...commonSpec,
|
||||
destination: {
|
||||
...commonSpec.destination,
|
||||
namespace: 'quarkus-app-preprod',
|
||||
},
|
||||
source: {
|
||||
...commonSpec.source,
|
||||
helm: {
|
||||
parameters: preProdHelmParameters,
|
||||
},
|
||||
},
|
||||
},
|
||||
status: {
|
||||
...commonStatus,
|
||||
history: [
|
||||
{
|
||||
...commonStatus.history[0],
|
||||
revision: '80f9758b7033a4bbb7c33a35ee474d61091644bc',
|
||||
deployedAt: '2024-04-22T05:39:24Z',
|
||||
source: {
|
||||
...commonStatus.history[0].source,
|
||||
helm: preProdHelmParameters,
|
||||
},
|
||||
},
|
||||
{
|
||||
...commonStatus.history[1],
|
||||
revision: '80f9758b7033a4bbb7c33a35ee474d61091644bc',
|
||||
source: {
|
||||
...commonStatus.history[0].source,
|
||||
helm: preProdHelmParameters,
|
||||
},
|
||||
},
|
||||
],
|
||||
operationState: {
|
||||
...commonStatus.operationState,
|
||||
operation: {
|
||||
...commonStatus.operationState.operation,
|
||||
sync: {
|
||||
...commonStatus.operationState.operation.sync,
|
||||
revision: '80f9758b7033a4bbb7c33a35ee474d61091644bc',
|
||||
},
|
||||
},
|
||||
},
|
||||
health: {
|
||||
status: 'Degraded',
|
||||
},
|
||||
sync: {
|
||||
status: 'Synced',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const prodHelmParameters = {
|
||||
parameters: [
|
||||
{
|
||||
name: 'namespace.name',
|
||||
value: 'quarkus-app-pre-prod',
|
||||
},
|
||||
{
|
||||
name: 'environment',
|
||||
value: 'prod',
|
||||
},
|
||||
{
|
||||
name: 'image.tag',
|
||||
value: 'prod',
|
||||
},
|
||||
],
|
||||
};
|
||||
export const prodApplication = {
|
||||
metadata: {
|
||||
creationTimestamp: new Date('2024-04-22T05:39:23Z'),
|
||||
labels: {
|
||||
'rht-gitops.com/janus-argocd': 'quarkus-app-bootstrap',
|
||||
},
|
||||
instance: { name: 'main' },
|
||||
name: 'quarkus-app-prod',
|
||||
},
|
||||
spec: {
|
||||
destination: {
|
||||
namespace: 'quarkus-app-prod',
|
||||
server: 'https://kubernetes.default.svc',
|
||||
},
|
||||
project: 'janus',
|
||||
source: {
|
||||
helm: prodHelmParameters,
|
||||
path: './helm/app',
|
||||
repoURL:
|
||||
'https://gitlab-gitlab.apps.cluster.test.com/prod/quarkus-app-gitops.git',
|
||||
targetRevision: 'HEAD',
|
||||
},
|
||||
},
|
||||
status: {
|
||||
history: [
|
||||
{
|
||||
...commonStatus.history[0],
|
||||
revision: '70f9758b7033a4bbb7c33a35ee474d61091644bc',
|
||||
deployedAt: '2024-04-19T05:39:24Z',
|
||||
source: {
|
||||
...commonStatus.history[0].source,
|
||||
helm: preProdHelmParameters,
|
||||
repoURL:
|
||||
'https://gitlab-gitlab.apps.cluster.test.com/prod/quarkus-app-gitops.git',
|
||||
},
|
||||
},
|
||||
{
|
||||
...commonStatus.history[1],
|
||||
revision: '70f9758b7033a4bbb7c33a35ee474d61091644bc',
|
||||
deployedAt: '2024-04-19T05:39:24Z',
|
||||
id: 1,
|
||||
source: {
|
||||
...commonStatus.history[1].source,
|
||||
helm: preProdHelmParameters,
|
||||
repoURL:
|
||||
'https://gitlab-gitlab.apps.cluster.test.com/prod/quarkus-app-gitops.git',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
operationState: {
|
||||
...commonStatus.operationState,
|
||||
operation: {
|
||||
...commonStatus.operationState.operation,
|
||||
sync: {
|
||||
...commonStatus.operationState.operation.sync,
|
||||
revision: '80f9758b7033a4bbb7c33a35ee474d61091644bc',
|
||||
},
|
||||
},
|
||||
},
|
||||
summary: {
|
||||
images: ['quay-hw6fw.apps.cluster.test.com/quayadmin/quarkus-app:latest'],
|
||||
},
|
||||
health: {
|
||||
status: 'Missing',
|
||||
},
|
||||
sync: {
|
||||
status: 'OutOfSync',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
export const mockArgocdConfig = {
|
||||
argocd: {
|
||||
baseUrl: 'https://localhost:8080',
|
||||
appLocatorMethods: [
|
||||
{
|
||||
type: 'config',
|
||||
instances: [
|
||||
{
|
||||
name: 'argoInstance1',
|
||||
url: 'https://test-openshift-gitops.apps.test.devcluster.openshift.com/',
|
||||
token: 'fake-jwt-token1',
|
||||
},
|
||||
{
|
||||
name: 'argoInstance2',
|
||||
url: 'https://test-openshift-gitops.apps.test.devcluster.openshift.com/',
|
||||
token: 'fake-jwt-token2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
export const mockEntity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'backstage',
|
||||
description: 'backstage.io',
|
||||
annotations: {
|
||||
'argocd/app-selector':
|
||||
'rht-gitops.com/janus-argocd=quarkus-app-bootstrap',
|
||||
'argocd/project-name': 'project-name',
|
||||
'argocd/instance-name': 'instance-1',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
lifecycle: 'production',
|
||||
type: 'service',
|
||||
owner: 'user:guest',
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export * from './applications';
|
||||
export * from './config';
|
||||
export * from './entity';
|
||||
export * from './revision';
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
export const mockRevision = {
|
||||
author: 'Karthik',
|
||||
date: '2023-10-10T05:28:38Z',
|
||||
message: 'First release',
|
||||
};
|
||||
|
||||
export const mockRevisionTwo = {
|
||||
author: 'Karthik',
|
||||
date: '2023-10-11T05:28:38Z',
|
||||
message: 'Commit v1.0.0 tag release',
|
||||
};
|
||||
|
||||
export const mockRevisionThree = {
|
||||
author: 'Karthik',
|
||||
date: '2023-10-13T05:28:38Z',
|
||||
message: 'Initial commit',
|
||||
};
|
||||
|
||||
export const mockRevisions = [mockRevision, mockRevisionTwo, mockRevisionThree];
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { configApiRef } from '@backstage/core-plugin-api';
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
import { permissionApiRef } from '@backstage/plugin-permission-react';
|
||||
import { MockPermissionApi, TestApiProvider } from '@backstage/test-utils';
|
||||
|
||||
import { Box } from '@material-ui/core';
|
||||
import { createDevAppThemes } from '@redhat-developer/red-hat-developer-hub-theme';
|
||||
|
||||
import {
|
||||
ArgoCDApi,
|
||||
argoCDApiRef,
|
||||
GetApplicationOptions,
|
||||
listAppsOptions,
|
||||
RevisionDetailsListOptions,
|
||||
RevisionDetailsOptions,
|
||||
} from '../src/api';
|
||||
import {
|
||||
ArgocdDeploymentLifecycle,
|
||||
ArgocdDeploymentSummary,
|
||||
argocdPlugin,
|
||||
} from '../src/plugin';
|
||||
import { Application } from '../src/types';
|
||||
import {
|
||||
mockApplication,
|
||||
mockArgocdConfig,
|
||||
mockRevision,
|
||||
mockRevisions,
|
||||
preProdApplication,
|
||||
prodApplication,
|
||||
} from './__data__';
|
||||
|
||||
const mockEntity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'backstage-argocd',
|
||||
description: 'rhtap argocd plugin',
|
||||
annotations: {
|
||||
'argocd/app-selector': 'rht.gitops.com/quarks-app-bootstrap',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
lifecycle: 'production',
|
||||
type: 'service',
|
||||
owner: 'user:guest',
|
||||
},
|
||||
};
|
||||
const mockPermissionApi = new MockPermissionApi();
|
||||
export class MockArgoCDApiClient implements ArgoCDApi {
|
||||
async listApps(_options: listAppsOptions): Promise<any> {
|
||||
return { items: [mockApplication, preProdApplication, prodApplication] };
|
||||
}
|
||||
|
||||
async getRevisionDetails(_options: RevisionDetailsOptions): Promise<any> {
|
||||
return mockRevision;
|
||||
}
|
||||
async getRevisionDetailsList(
|
||||
_options: RevisionDetailsListOptions,
|
||||
): Promise<any> {
|
||||
return mockRevisions;
|
||||
}
|
||||
async getApplication(_options: GetApplicationOptions): Promise<Application> {
|
||||
return mockApplication;
|
||||
}
|
||||
}
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin(argocdPlugin)
|
||||
.addThemes(createDevAppThemes())
|
||||
.addPage({
|
||||
element: (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[configApiRef, new ConfigReader(mockArgocdConfig)],
|
||||
[argoCDApiRef, new MockArgoCDApiClient()],
|
||||
[permissionApiRef, mockPermissionApi],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={mockEntity}>
|
||||
<Box margin={2}>
|
||||
<ArgocdDeploymentLifecycle />
|
||||
</Box>
|
||||
</EntityProvider>
|
||||
</TestApiProvider>
|
||||
),
|
||||
title: 'Lifecycle',
|
||||
path: '/argocd/deployment-lifecycle',
|
||||
})
|
||||
|
||||
.addPage({
|
||||
element: (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[configApiRef, new ConfigReader(mockArgocdConfig)],
|
||||
[argoCDApiRef, new MockArgoCDApiClient()],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={mockEntity}>
|
||||
<ArgocdDeploymentSummary />
|
||||
</EntityProvider>
|
||||
</TestApiProvider>
|
||||
),
|
||||
title: 'Summary',
|
||||
path: 'argocd/deployment-summary',
|
||||
})
|
||||
.render();
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
{
|
||||
"name": "@backstage-community/plugin-redhat-argocd",
|
||||
"version": "1.6.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "frontend-plugin",
|
||||
"supported-versions": "1.28.4",
|
||||
"pluginId": "redhat-argocd",
|
||||
"pluginPackage": "@backstage-community/plugin-redhat-argocd",
|
||||
"pluginPackages": [
|
||||
"@backstage-community/plugin-redhat-argocd"
|
||||
]
|
||||
},
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"export-dynamic": "janus-cli package export-dynamic-plugin --in-place",
|
||||
"lint": "backstage-cli package lint",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"postversion": "yarn run export-dynamic",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"start": "backstage-cli package start",
|
||||
"test": "backstage-cli package test --passWithNoTests --coverage",
|
||||
"tsc": "tsc",
|
||||
"ui-test": "yarn playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^1.5.0",
|
||||
"@backstage/core-components": "^0.14.9",
|
||||
"@backstage/core-plugin-api": "^1.9.3",
|
||||
"@backstage/plugin-catalog-react": "^1.12.2",
|
||||
"@backstage/plugin-permission-react": "^0.4.24",
|
||||
"@backstage/theme": "^0.5.6",
|
||||
"@janus-idp/backstage-plugin-argocd-common": "1.0.1",
|
||||
"@kubernetes/client-node": "^0.20.0",
|
||||
"@material-ui/core": "^4.9.13",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"@patternfly/patternfly": "^5.1.0",
|
||||
"@patternfly/react-charts": "^7.1.1",
|
||||
"@patternfly/react-core": "^5.1.2",
|
||||
"@patternfly/react-icons": "^5.1.1",
|
||||
"@patternfly/react-tokens": "^5.1.2",
|
||||
"moment": "^2.30.1",
|
||||
"react-use": "17.5.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
"react-router-dom": "^6.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "0.26.11",
|
||||
"@backstage/config": "1.2.0",
|
||||
"@backstage/core-app-api": "1.14.1",
|
||||
"@backstage/dev-utils": "1.0.36",
|
||||
"@backstage/test-utils": "1.5.9",
|
||||
"@janus-idp/cli": "1.13.0",
|
||||
"@playwright/test": "1.45.3",
|
||||
"@redhat-developer/red-hat-developer-hub-theme": "0.0.54",
|
||||
"@testing-library/jest-dom": "6.4.8",
|
||||
"@testing-library/react": "14.3.1",
|
||||
"@testing-library/user-event": "14.5.2",
|
||||
"msw": "1.3.3",
|
||||
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^18",
|
||||
"react-router-dom": "^6.3.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"config.d.ts",
|
||||
"dist-scalprum",
|
||||
"app-config.janus-idp.yaml"
|
||||
],
|
||||
"scalprum": {
|
||||
"name": "janus-idp.backstage-plugin-argocd",
|
||||
"exposedModules": {
|
||||
"PluginRoot": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"configSchema": "config.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/community-plugins",
|
||||
"directory": "workspaces/redhat-argocd/plugins/argocd"
|
||||
},
|
||||
"keywords": [
|
||||
"support:production",
|
||||
"lifecycle:active",
|
||||
"backstage",
|
||||
"plugin"
|
||||
],
|
||||
"homepage": "https://red.ht/rhdh",
|
||||
"bugs": "https://github.com/backstage/community-plugins/issues",
|
||||
"maintainers": [
|
||||
"karthikjeeyar",
|
||||
"rohitkrai03",
|
||||
"Eswaraiahsapram"
|
||||
],
|
||||
"author": "Red Hat"
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Run tests in sequence. */
|
||||
workers: 1,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
baseURL: process.env.PLUGIN_BASE_URL || 'http://localhost:3000',
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
|
||||
import { ArgoCDApiClient } from '..';
|
||||
import { mockApplication } from '../../../dev/__data__';
|
||||
|
||||
const getIdentityApiStub: IdentityApi = {
|
||||
getProfileInfo: jest.fn(),
|
||||
getBackstageIdentity: jest.fn(),
|
||||
async getCredentials() {
|
||||
return { token: 'fake-jwt-token' };
|
||||
},
|
||||
signOut: jest.fn(),
|
||||
};
|
||||
|
||||
describe('API calls', () => {
|
||||
let fetchSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
fetchSpy = jest.spyOn(global, 'fetch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetchSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('listApps', () => {
|
||||
beforeEach(() => {
|
||||
fetchSpy.mockImplementation(() =>
|
||||
Promise.resolve<Response>({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ items: [] }),
|
||||
} as Response),
|
||||
);
|
||||
});
|
||||
|
||||
test('fetches app based on provided projectName', async () => {
|
||||
const client = new ArgoCDApiClient({
|
||||
backendBaseUrl: 'https://test.com',
|
||||
useNamespacedApps: false,
|
||||
identityApi: getIdentityApiStub,
|
||||
});
|
||||
|
||||
await client.listApps({
|
||||
url: '',
|
||||
appSelector: 'janus.io%253Dquarkus-app',
|
||||
projectName: 'test',
|
||||
appNamespace: 'my-test-ns',
|
||||
});
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://test.com/api/argocd/applications/selector/janus.io%253Dquarkus-app?selector=janus.io%25253Dquarkus-app&project=test',
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer fake-jwt-token`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
test('fetches app based on provided appSelector', async () => {
|
||||
const client = new ArgoCDApiClient({
|
||||
backendBaseUrl: 'https://test.com',
|
||||
useNamespacedApps: false,
|
||||
identityApi: getIdentityApiStub,
|
||||
});
|
||||
|
||||
await client.listApps({
|
||||
url: '',
|
||||
appSelector: 'my-test-app-selector',
|
||||
});
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://test.com/api/argocd/applications/selector/my-test-app-selector?selector=my-test-app-selector',
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer fake-jwt-token`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('Should throw error incase of any internal API failure', async () => {
|
||||
fetchSpy.mockImplementation(() =>
|
||||
Promise.resolve<Response>({
|
||||
ok: false,
|
||||
status: 'Internal server error',
|
||||
statusText: 'Something went wrong',
|
||||
json: () => Promise.reject({ status: 'Internal server error' }),
|
||||
} as unknown as Response),
|
||||
);
|
||||
|
||||
const client = new ArgoCDApiClient({
|
||||
backendBaseUrl: 'https://test.com',
|
||||
useNamespacedApps: false,
|
||||
identityApi: getIdentityApiStub,
|
||||
});
|
||||
let error;
|
||||
try {
|
||||
await client.listApps({
|
||||
url: '',
|
||||
appSelector: 'my-test-app-selector',
|
||||
});
|
||||
} catch (e: any) {
|
||||
error = e;
|
||||
} finally {
|
||||
expect(error.message).toBe(
|
||||
'failed to fetch data, status Internal server error: Something went wrong',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('should not pass the token for the guest user', async () => {
|
||||
const client = new ArgoCDApiClient({
|
||||
backendBaseUrl: 'https://test.com',
|
||||
useNamespacedApps: false,
|
||||
identityApi: {
|
||||
...getIdentityApiStub,
|
||||
getCredentials: async () => {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await client.listApps({
|
||||
url: '',
|
||||
projectName: 'test',
|
||||
appSelector: 'janus.io%253Dquarkus-app',
|
||||
appNamespace: 'my-test-ns',
|
||||
});
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://test.com/api/argocd/applications/selector/janus.io%253Dquarkus-app?selector=janus.io%25253Dquarkus-app&project=test',
|
||||
expect.objectContaining({
|
||||
headers: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getApplication', () => {
|
||||
beforeEach(() => {
|
||||
fetchSpy.mockImplementation(() =>
|
||||
Promise.resolve<Response>({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
} as Response),
|
||||
);
|
||||
});
|
||||
|
||||
test('should return empty object as response', async () => {
|
||||
const client = new ArgoCDApiClient({
|
||||
backendBaseUrl: 'https://test.com',
|
||||
useNamespacedApps: false,
|
||||
identityApi: getIdentityApiStub,
|
||||
});
|
||||
|
||||
const data = await client.getApplication({
|
||||
url: '',
|
||||
appName: 'quarkus-app',
|
||||
appNamespace: '',
|
||||
});
|
||||
expect(Object.keys(data)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should return application object', async () => {
|
||||
fetchSpy.mockImplementation(() =>
|
||||
Promise.resolve<Response>({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockApplication),
|
||||
} as Response),
|
||||
);
|
||||
|
||||
const client = new ArgoCDApiClient({
|
||||
backendBaseUrl: 'https://test.com',
|
||||
useNamespacedApps: false,
|
||||
identityApi: getIdentityApiStub,
|
||||
});
|
||||
|
||||
const data = await client.getApplication({
|
||||
url: '',
|
||||
appName: 'quarkus-app',
|
||||
appNamespace: '',
|
||||
});
|
||||
expect(data).toEqual(
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({
|
||||
name: 'quarkus-app-dev',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRevisionDetails', () => {
|
||||
beforeEach(() => {
|
||||
fetchSpy.mockImplementation(() =>
|
||||
Promise.resolve<Response>({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ items: [] }),
|
||||
} as Response),
|
||||
);
|
||||
});
|
||||
|
||||
test('should return the revision details', async () => {
|
||||
const client = new ArgoCDApiClient({
|
||||
backendBaseUrl: 'https://test.com',
|
||||
useNamespacedApps: false,
|
||||
identityApi: getIdentityApiStub,
|
||||
});
|
||||
|
||||
await client.getRevisionDetails({
|
||||
instanceName: 'main',
|
||||
app: 'my-test-app',
|
||||
revisionID: '12345',
|
||||
});
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://test.com/api/argocd/argoInstance/main/applications/name/my-test-app/revisions/12345/metadata',
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer fake-jwt-token`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRevisionDetailsList', () => {
|
||||
beforeEach(() => {
|
||||
fetchSpy.mockImplementation(() =>
|
||||
Promise.resolve<Response>({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ items: [] }),
|
||||
} as Response),
|
||||
);
|
||||
});
|
||||
|
||||
test('should return empty list if the revisionIds are not passed', async () => {
|
||||
const client = new ArgoCDApiClient({
|
||||
backendBaseUrl: 'https://test.com',
|
||||
useNamespacedApps: false,
|
||||
identityApi: getIdentityApiStub,
|
||||
});
|
||||
|
||||
const data = await client.getRevisionDetailsList({
|
||||
instanceName: 'main',
|
||||
apps: [mockApplication],
|
||||
revisionIDs: [],
|
||||
appNamespace: '',
|
||||
});
|
||||
expect(data).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should return the list of revision details', async () => {
|
||||
const client = new ArgoCDApiClient({
|
||||
backendBaseUrl: 'https://test.com',
|
||||
useNamespacedApps: false,
|
||||
identityApi: getIdentityApiStub,
|
||||
});
|
||||
|
||||
await client.getRevisionDetailsList({
|
||||
instanceName: 'main',
|
||||
apps: [mockApplication],
|
||||
revisionIDs: ['90f9758b7033a4bbb7c33a35ee474d61091644bc'],
|
||||
appNamespace: '',
|
||||
});
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://test.com/api/argocd/argoInstance/main/applications/name/quarkus-app-dev/revisions/90f9758b7033a4bbb7c33a35ee474d61091644bc/metadata',
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer fake-jwt-token`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
import { createApiRef, IdentityApi } from '@backstage/core-plugin-api';
|
||||
|
||||
import { Application, Revision } from '../types';
|
||||
|
||||
export type ArgoCDAppDeployRevisionDetails = Revision;
|
||||
|
||||
export type listAppsOptions = {
|
||||
url: string;
|
||||
appSelector?: string;
|
||||
appNamespace?: string;
|
||||
projectName?: string;
|
||||
};
|
||||
|
||||
export type RevisionDetailsOptions = {
|
||||
app: string;
|
||||
appNamespace?: string;
|
||||
revisionID: string;
|
||||
instanceName?: string;
|
||||
};
|
||||
export type RevisionDetailsListOptions = {
|
||||
appNamespace?: string;
|
||||
revisionIDs: string[];
|
||||
instanceName?: string;
|
||||
apps: Application[];
|
||||
};
|
||||
|
||||
export type GetApplicationOptions = {
|
||||
url: string;
|
||||
appName: string;
|
||||
appNamespace?: string;
|
||||
instance?: string;
|
||||
};
|
||||
export interface ArgoCDApi {
|
||||
listApps(options: listAppsOptions): Promise<{ items: Application[] }>;
|
||||
getRevisionDetails(
|
||||
options: RevisionDetailsOptions,
|
||||
): Promise<ArgoCDAppDeployRevisionDetails>;
|
||||
getRevisionDetailsList(
|
||||
options: RevisionDetailsListOptions,
|
||||
): Promise<ArgoCDAppDeployRevisionDetails[]>;
|
||||
getApplication(options: GetApplicationOptions): Promise<Application>;
|
||||
}
|
||||
|
||||
export const argoCDApiRef = createApiRef<ArgoCDApi>({
|
||||
id: 'plugin.argo.cd.service',
|
||||
});
|
||||
|
||||
export type Options = {
|
||||
backendBaseUrl: string;
|
||||
identityApi: IdentityApi;
|
||||
proxyPath?: string;
|
||||
useNamespacedApps: boolean;
|
||||
};
|
||||
|
||||
const APP_NAMESPACE_QUERY_PARAM = 'appNamespace';
|
||||
|
||||
export class ArgoCDApiClient implements ArgoCDApi {
|
||||
private readonly backendBaseUrl: string;
|
||||
private readonly identityApi: IdentityApi;
|
||||
private readonly useNamespacedApps: boolean;
|
||||
|
||||
constructor(options: Options) {
|
||||
this.backendBaseUrl = options.backendBaseUrl;
|
||||
this.identityApi = options.identityApi;
|
||||
this.useNamespacedApps = options.useNamespacedApps;
|
||||
}
|
||||
|
||||
async getBaseUrl() {
|
||||
return `${this.backendBaseUrl}/api/argocd`;
|
||||
}
|
||||
|
||||
getQueryParams(params: { [p: string]: string | undefined }) {
|
||||
const result = Object.keys(params)
|
||||
.filter(key => params[key] !== undefined)
|
||||
.filter(
|
||||
key => key !== APP_NAMESPACE_QUERY_PARAM || this.useNamespacedApps,
|
||||
)
|
||||
.map(
|
||||
k =>
|
||||
`${encodeURIComponent(k)}=${encodeURIComponent(params[k] as string)}`,
|
||||
)
|
||||
.join('&');
|
||||
return result ? `?${result}` : '';
|
||||
}
|
||||
|
||||
async fetcher(url: string) {
|
||||
const { token } = await this.identityApi.getCredentials();
|
||||
const response = await fetch(url, {
|
||||
headers: token
|
||||
? {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`failed to fetch data, status ${response.status}: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
const json = await response.json();
|
||||
return json;
|
||||
}
|
||||
|
||||
async listApps(options: {
|
||||
url: string;
|
||||
appSelector?: string;
|
||||
appNamespace?: string;
|
||||
projectName?: string;
|
||||
}) {
|
||||
const proxyUrl = await this.getBaseUrl();
|
||||
const query = this.getQueryParams({
|
||||
selector: options.appSelector,
|
||||
project: options.projectName,
|
||||
appNamespace: options.appNamespace,
|
||||
});
|
||||
return this.fetcher(
|
||||
`${proxyUrl}${options.url}/applications/selector/${options.appSelector}${query}`,
|
||||
);
|
||||
}
|
||||
|
||||
async getApplication(options: GetApplicationOptions) {
|
||||
const proxyUrl = await this.getBaseUrl();
|
||||
const query = this.getQueryParams({
|
||||
appNamespace: options.appNamespace,
|
||||
});
|
||||
return this.fetcher(
|
||||
`${proxyUrl}${options.url}/applications/${encodeURIComponent(
|
||||
options.appName,
|
||||
)}${query}`,
|
||||
);
|
||||
}
|
||||
|
||||
async getRevisionDetails(options: {
|
||||
app: string;
|
||||
appNamespace?: string;
|
||||
revisionID: string;
|
||||
instanceName: string;
|
||||
}) {
|
||||
const proxyUrl = await this.getBaseUrl();
|
||||
|
||||
const query = this.getQueryParams({
|
||||
appNamespace: options.appNamespace,
|
||||
});
|
||||
return this.fetcher(
|
||||
`${proxyUrl}/argoInstance/${
|
||||
options.instanceName
|
||||
}/applications/name/${encodeURIComponent(
|
||||
options.app,
|
||||
)}/revisions/${encodeURIComponent(options.revisionID)}/metadata${query}`,
|
||||
);
|
||||
}
|
||||
|
||||
async getRevisionDetailsList(options: {
|
||||
appNamespace: string;
|
||||
revisionIDs: string[];
|
||||
instanceName: string;
|
||||
apps: Application[];
|
||||
}): Promise<Revision[]> {
|
||||
if (!options.revisionIDs || options.revisionIDs.length < 1) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
const promises: any = [];
|
||||
options.revisionIDs.forEach((revisionID: string) => {
|
||||
const application = options.apps.find(app =>
|
||||
app?.status?.history?.find(h => h.revision === revisionID),
|
||||
);
|
||||
if (application) {
|
||||
promises.push(
|
||||
this.getRevisionDetails({
|
||||
app: application.metadata.name as string,
|
||||
appNamespace: options.appNamespace,
|
||||
instanceName: options.instanceName,
|
||||
revisionID,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Chip } from '@material-ui/core';
|
||||
|
||||
import { Application, HealthStatus } from '../../types';
|
||||
import { AppHealthIcon } from './StatusIcons';
|
||||
|
||||
const AppHealthStatus: React.FC<{ app: Application; isChip?: boolean }> = ({
|
||||
app,
|
||||
isChip = false,
|
||||
}) => {
|
||||
return isChip ? (
|
||||
<Chip
|
||||
data-testid="app-health-status-chip"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
icon={<AppHealthIcon status={app.status.health.status as HealthStatus} />}
|
||||
label={app.status.health.status}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<AppHealthIcon status={app.status.health.status as HealthStatus} />{' '}
|
||||
{app.status.health.status}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppHealthStatus;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Chip, Typography } from '@material-ui/core';
|
||||
import { Flex, FlexItem } from '@patternfly/react-core';
|
||||
|
||||
import { Application } from '../../types';
|
||||
|
||||
const AppNamespace: React.FC<{ app: Application }> = ({ app }) => {
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Flex
|
||||
gap={{ default: 'gapNone' }}
|
||||
alignItems={{ default: 'alignItemsFlexStart' }}
|
||||
>
|
||||
<FlexItem>
|
||||
<Chip
|
||||
size="small"
|
||||
variant="default"
|
||||
color="primary"
|
||||
label="NS"
|
||||
style={{ background: 'green' }}
|
||||
/>
|
||||
</FlexItem>
|
||||
<FlexItem>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{app.spec.destination.namespace}{' '}
|
||||
</Typography>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppNamespace;
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Chip } from '@material-ui/core';
|
||||
|
||||
import { Application, SyncStatusCode } from '../../types';
|
||||
import { SyncIcon } from './StatusIcons';
|
||||
|
||||
const AppSyncStatus: React.FC<{
|
||||
app: Application;
|
||||
isChip?: boolean;
|
||||
}> = ({ app, isChip = false }) => {
|
||||
return isChip ? (
|
||||
<Chip
|
||||
data-testid="app-sync-status-chip"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
icon={<SyncIcon status={app?.status?.sync?.status as SyncStatusCode} />}
|
||||
label={app?.status?.sync?.status}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<SyncIcon status={app?.status?.sync?.status as SyncStatusCode} />{' '}
|
||||
{app?.status?.sync?.status}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default AppSyncStatus;
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Application } from '../../types';
|
||||
import AppHealthStatus from './AppHealthStatus';
|
||||
import AppSyncStatus from './AppSyncStatus';
|
||||
|
||||
const StatusHeading: React.FC<{ app: Application }> = ({ app }) => {
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<AppSyncStatus app={app} isChip />
|
||||
<AppHealthStatus app={app} isChip />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusHeading;
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
import React from 'react';
|
||||
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core';
|
||||
import ArrowCircleUpIcon from '@patternfly/react-icons/dist/esm/icons/arrow-alt-circle-up-icon';
|
||||
import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon';
|
||||
import CircleNotchIcon from '@patternfly/react-icons/dist/esm/icons/circle-notch-icon';
|
||||
import GhostIcon from '@patternfly/react-icons/dist/esm/icons/ghost-icon';
|
||||
import HeartBrokenIcon from '@patternfly/react-icons/dist/esm/icons/heart-broken-icon';
|
||||
import HeartIcon from '@patternfly/react-icons/dist/esm/icons/heart-icon';
|
||||
import PauseCircleIcon from '@patternfly/react-icons/dist/esm/icons/pause-circle-icon';
|
||||
import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon';
|
||||
|
||||
import { HealthStatus, SyncStatusCode, SyncStatuses } from '../../types';
|
||||
|
||||
const useIconStyles = makeStyles<Theme>(theme =>
|
||||
createStyles({
|
||||
icon: {
|
||||
marginLeft: theme.spacing(0.6),
|
||||
},
|
||||
'icon-spin': {
|
||||
animation: '$spin-animation 0.5s infinite',
|
||||
display: 'inline-block',
|
||||
},
|
||||
|
||||
'@keyframes spin-animation': {
|
||||
'0%': {
|
||||
transform: 'rotate(0deg)',
|
||||
},
|
||||
'100%': {
|
||||
transform: 'rotate(359deg)',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
export const SyncIcon: React.FC<{ status: SyncStatusCode }> = ({
|
||||
status,
|
||||
}): React.ReactNode => {
|
||||
const classes = useIconStyles();
|
||||
switch (status) {
|
||||
case SyncStatuses.Synced:
|
||||
return (
|
||||
<CheckCircleIcon
|
||||
data-testid="synced-icon"
|
||||
className={`${classes.icon}`}
|
||||
style={{ height: '1em', color: 'green' }}
|
||||
/>
|
||||
);
|
||||
case SyncStatuses.OutOfSync:
|
||||
return (
|
||||
<ArrowCircleUpIcon
|
||||
data-testid="outofsync-icon"
|
||||
className={`${classes.icon}`}
|
||||
style={{ height: '1em', color: '#f4c030' }}
|
||||
/>
|
||||
);
|
||||
case SyncStatuses.Unknown:
|
||||
return (
|
||||
<CircleNotchIcon
|
||||
data-testid="unknown-icon"
|
||||
className={`${classes.icon} ${classes['icon-spin']}`}
|
||||
style={{ height: '1em', color: '#0DADEA' }}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const AppHealthIcon: React.FC<{ status: HealthStatus }> = ({
|
||||
status,
|
||||
}): React.ReactNode => {
|
||||
const classes = useIconStyles();
|
||||
|
||||
switch (status) {
|
||||
case HealthStatus.Healthy:
|
||||
return (
|
||||
<HeartIcon
|
||||
data-testid="healthy-icon"
|
||||
className={`${classes.icon}`}
|
||||
style={{ height: '1em', color: 'green' }}
|
||||
/>
|
||||
);
|
||||
case HealthStatus.Suspended:
|
||||
return (
|
||||
<PauseCircleIcon
|
||||
data-testid="suspended-icon"
|
||||
className={`${classes.icon}`}
|
||||
style={{ height: '1em', color: '#766f94' }}
|
||||
/>
|
||||
);
|
||||
case HealthStatus.Degraded:
|
||||
return (
|
||||
<HeartBrokenIcon
|
||||
data-testid="degraded-icon"
|
||||
className={`${classes.icon}`}
|
||||
style={{ height: '1em', color: '#E96D76' }}
|
||||
/>
|
||||
);
|
||||
case HealthStatus.Progressing:
|
||||
return (
|
||||
<CircleNotchIcon
|
||||
data-testid="progressing-icon"
|
||||
className={`${classes.icon} ${classes['icon-spin']}`}
|
||||
style={{ height: '1em', color: '#0DADEA' }}
|
||||
/>
|
||||
);
|
||||
case HealthStatus.Missing:
|
||||
return (
|
||||
<GhostIcon
|
||||
data-testid="missing-icon"
|
||||
className={`${classes.icon}`}
|
||||
style={{ height: '1em', color: '#f4c030' }}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<QuestionCircleIcon
|
||||
data-testid="unknown-icon"
|
||||
style={{ height: '1em', color: 'green' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { mockApplication } from '../../../../dev/__data__';
|
||||
import AppHealthStatus from '../AppHealthStatus';
|
||||
|
||||
describe('AppHealthStatus', () => {
|
||||
test('should return default component', () => {
|
||||
render(<AppHealthStatus app={mockApplication} />);
|
||||
|
||||
expect(screen.queryByTestId('healthy-icon')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Healthy')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should return application health chip component', () => {
|
||||
render(<AppHealthStatus app={mockApplication} isChip />);
|
||||
|
||||
expect(screen.getByTestId('app-health-status-chip')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { mockApplication } from '../../../../dev/__data__';
|
||||
import { Application } from '../../../types';
|
||||
import AppNamespace from '../AppNamespace';
|
||||
|
||||
describe('AppNamespace', () => {
|
||||
test('should not render if the application is not available', () => {
|
||||
render(<AppNamespace app={null as unknown as Application} />);
|
||||
expect(
|
||||
screen.queryByText(mockApplication.spec.destination.namespace),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the namespace application is available', () => {
|
||||
render(<AppNamespace app={mockApplication} />);
|
||||
|
||||
expect(
|
||||
screen.queryByText(mockApplication.spec.destination.namespace),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { mockApplication } from '../../../../dev/__data__';
|
||||
import AppSyncStatus from '../AppSyncStatus';
|
||||
|
||||
describe('AppSyncStatus', () => {
|
||||
test('should return default component', () => {
|
||||
render(<AppSyncStatus app={mockApplication} />);
|
||||
|
||||
expect(screen.getByTestId('synced-icon')).toBeInTheDocument();
|
||||
expect(screen.getByText('Synced')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should return application health chip component', () => {
|
||||
render(<AppSyncStatus app={mockApplication} isChip />);
|
||||
|
||||
expect(screen.getByTestId('app-sync-status-chip')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { mockApplication } from '../../../../dev/__data__';
|
||||
import { Application } from '../../../types';
|
||||
import StatusHeading from '../StatusHeading';
|
||||
|
||||
describe('StatusHeading', () => {
|
||||
test('should not render if the application is not available', () => {
|
||||
render(<StatusHeading app={null as unknown as Application} />);
|
||||
expect(screen.queryByText('app-sync-status-chip')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render if the application is available', () => {
|
||||
render(<StatusHeading app={mockApplication} />);
|
||||
|
||||
expect(screen.queryByTestId('app-health-status-chip')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('app-sync-status-chip')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { HealthStatus, SyncStatusCode } from '../../../types';
|
||||
import { AppHealthIcon, SyncIcon } from '../StatusIcons';
|
||||
|
||||
describe('StatusIcons', () => {
|
||||
describe('Sync Status', () => {
|
||||
test('should return application sync icon', () => {
|
||||
render(<SyncIcon status="Synced" />);
|
||||
expect(screen.getByTestId('synced-icon')).toBeInTheDocument();
|
||||
|
||||
render(<SyncIcon status="OutOfSync" />);
|
||||
expect(screen.getByTestId('outofsync-icon')).toBeInTheDocument();
|
||||
|
||||
render(<SyncIcon status="Unknown" />);
|
||||
expect(screen.getByTestId('unknown-icon')).toBeInTheDocument();
|
||||
|
||||
render(<SyncIcon status={'invalid' as SyncStatusCode} />);
|
||||
expect(screen.getByTestId('unknown-icon')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Health Status', () => {
|
||||
test('should return application health status icon', () => {
|
||||
render(<AppHealthIcon status={HealthStatus.Healthy} />);
|
||||
expect(screen.getByTestId('healthy-icon')).toBeInTheDocument();
|
||||
|
||||
render(<AppHealthIcon status={HealthStatus.Suspended} />);
|
||||
expect(screen.getByTestId('suspended-icon')).toBeInTheDocument();
|
||||
|
||||
render(<AppHealthIcon status={HealthStatus.Progressing} />);
|
||||
expect(screen.getByTestId('progressing-icon')).toBeInTheDocument();
|
||||
|
||||
render(<AppHealthIcon status={HealthStatus.Missing} />);
|
||||
expect(screen.getByTestId('missing-icon')).toBeInTheDocument();
|
||||
|
||||
render(<AppHealthIcon status={HealthStatus.Degraded} />);
|
||||
expect(screen.getByTestId('degraded-icon')).toBeInTheDocument();
|
||||
|
||||
render(<AppHealthIcon status={HealthStatus.Unknown} />);
|
||||
expect(screen.getByTestId('unknown-icon')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Alert, AlertTitle } from '@material-ui/lab';
|
||||
|
||||
const PermissionAlert = () => {
|
||||
return (
|
||||
<Alert severity="warning" data-testid="no-permission-alert">
|
||||
<AlertTitle>Permission required</AlertTitle>
|
||||
To view argocd plugin, contact your administrator to give you the
|
||||
argocd.view.read permission.
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
export default PermissionAlert;
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { Progress, ResponseErrorPanel } from '@backstage/core-components';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
|
||||
import { createStyles, makeStyles, Theme, Typography } from '@material-ui/core';
|
||||
|
||||
import { argoCDApiRef } from '../../api';
|
||||
import { useApplications } from '../../hooks/useApplications';
|
||||
import { useArgocdConfig } from '../../hooks/useArgocdConfig';
|
||||
import { useArgocdViewPermission } from '../../hooks/useArgocdViewPermission';
|
||||
import { Application, Revision } from '../../types';
|
||||
import {
|
||||
getArgoCdAppConfig,
|
||||
getInstanceName,
|
||||
getUniqueRevisions,
|
||||
} from '../../utils/utils';
|
||||
import PermissionAlert from '../Common/PermissionAlert';
|
||||
import DeploymentLifecycleCard from './DeploymentLifecycleCard';
|
||||
import DeploymentLifecycleDrawer from './DeploymentLifecycleDrawer';
|
||||
|
||||
const useDrawerStyles = makeStyles<Theme>(theme =>
|
||||
createStyles({
|
||||
lifecycle: {
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
overflowX: 'auto',
|
||||
background:
|
||||
theme.palette.type === 'dark'
|
||||
? theme.palette.grey[700]
|
||||
: theme.palette.grey[200],
|
||||
color: 'black',
|
||||
margin: '1px solid red',
|
||||
padding: '20px',
|
||||
borderRadius: '10px',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const DeploymentLifecycle = () => {
|
||||
const { entity } = useEntity();
|
||||
const classes = useDrawerStyles();
|
||||
|
||||
const api = useApi(argoCDApiRef);
|
||||
|
||||
const { instances, intervalMs } = useArgocdConfig();
|
||||
const instanceName = getInstanceName(entity) || instances?.[0]?.name;
|
||||
const { appSelector, appName, projectName, appNamespace } =
|
||||
getArgoCdAppConfig({ entity });
|
||||
|
||||
const { apps, loading, error } = useApplications({
|
||||
instanceName,
|
||||
intervalMs,
|
||||
appSelector,
|
||||
appName,
|
||||
appNamespace,
|
||||
projectName,
|
||||
});
|
||||
|
||||
const hasArgocdViewAccess = useArgocdViewPermission();
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [activeItem, setActiveItem] = React.useState<string>();
|
||||
const [, setRevisions] = React.useState<{
|
||||
[key: string]: Revision;
|
||||
}>();
|
||||
const revisionCache = React.useRef<{ [key: string]: Revision }>({});
|
||||
|
||||
const uniqRevisions: string[] = React.useMemo(
|
||||
() => getUniqueRevisions(apps),
|
||||
[apps],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (uniqRevisions.length !== Object.keys(revisionCache.current).length) {
|
||||
api
|
||||
.getRevisionDetailsList({
|
||||
apps: apps,
|
||||
instanceName,
|
||||
revisionIDs: uniqRevisions,
|
||||
})
|
||||
.then(data => {
|
||||
uniqRevisions.forEach((rev, i) => {
|
||||
revisionCache.current[rev] = data[i];
|
||||
});
|
||||
setRevisions(revisionCache.current);
|
||||
})
|
||||
.catch(e => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(e);
|
||||
});
|
||||
}
|
||||
}, [api, apps, entity, instanceName, uniqRevisions]);
|
||||
|
||||
const toggleDrawer = () => setOpen(e => !e);
|
||||
|
||||
const activeApp = apps.find(a => a.metadata.name === activeItem);
|
||||
|
||||
if (!hasArgocdViewAccess) {
|
||||
return <PermissionAlert />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ResponseErrorPanel data-testid="error-panel" error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div data-testid="argocd-loader">
|
||||
<Progress />
|
||||
</div>
|
||||
);
|
||||
} else if (apps?.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h2" gutterBottom>
|
||||
Deployment lifecycle
|
||||
</Typography>
|
||||
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Review deployed components/systems in the namespace using ArgoCD plugin
|
||||
</Typography>
|
||||
|
||||
<div className={classes.lifecycle}>
|
||||
{apps.map((app: Application, idx: number) => (
|
||||
<DeploymentLifecycleCard
|
||||
app={app}
|
||||
key={app.metadata.uid ?? idx}
|
||||
revisionsMap={revisionCache.current}
|
||||
onclick={() => {
|
||||
toggleDrawer();
|
||||
setActiveItem(app.metadata.name);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<DeploymentLifecycleDrawer
|
||||
app={activeApp}
|
||||
isOpen={open}
|
||||
onClose={() => setOpen(false)}
|
||||
revisionsMap={revisionCache.current}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeploymentLifecycle;
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Chip,
|
||||
createStyles,
|
||||
Divider,
|
||||
Grid,
|
||||
Link,
|
||||
makeStyles,
|
||||
Theme,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import { Skeleton } from '@material-ui/lab';
|
||||
import GitLabIcon from '@patternfly/react-icons/dist/esm/icons/gitlab-icon';
|
||||
import moment from 'moment';
|
||||
|
||||
import { Application, Revision } from '../../types';
|
||||
import { getCommitUrl, isAppHelmChartType } from '../../utils/utils';
|
||||
import AppNamespace from '../AppStatus/AppNamespace';
|
||||
import StatusHeading from '../AppStatus/StatusHeading';
|
||||
import DeploymentLifecycleHeader from './DeploymentLifecycleHeader';
|
||||
|
||||
const useCardStyles = makeStyles<Theme>(theme =>
|
||||
createStyles({
|
||||
card: {
|
||||
flex: '0 0 auto',
|
||||
marginRight: theme.spacing(2.5),
|
||||
maxWidth: '300px',
|
||||
},
|
||||
commitMessage: {
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
interface DeploymentLifecycleCardProps {
|
||||
app: Application;
|
||||
revisionsMap: { [key: string]: Revision };
|
||||
onclick?: () => void;
|
||||
}
|
||||
|
||||
const DeploymentLifecycleCard: React.FC<DeploymentLifecycleCardProps> = ({
|
||||
app,
|
||||
onclick,
|
||||
revisionsMap,
|
||||
}) => {
|
||||
const appHistory = app?.status?.history ?? [];
|
||||
const latestRevision = appHistory[appHistory.length - 1];
|
||||
const appDeployedAt = latestRevision?.deployedAt;
|
||||
|
||||
const classes = useCardStyles();
|
||||
const { entity } = useEntity();
|
||||
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
data-testid={`${app?.metadata?.name}-card`}
|
||||
key={app?.metadata?.uid}
|
||||
className={classes.card}
|
||||
style={{ justifyContent: 'space-between' }}
|
||||
onClick={onclick}
|
||||
>
|
||||
<CardHeader
|
||||
title={<DeploymentLifecycleHeader app={app} />}
|
||||
titleTypographyProps={{
|
||||
variant: 'subtitle2',
|
||||
}}
|
||||
subheader={<StatusHeading app={app} />}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
<CardContent>
|
||||
<Grid container spacing={1} alignItems="flex-start">
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body1">Instance</Typography>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
{app?.metadata?.instance?.name}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body1">Server</Typography>
|
||||
|
||||
<Typography
|
||||
style={{ overflowWrap: 'anywhere' }}
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
gutterBottom
|
||||
>
|
||||
{app?.spec?.destination?.server}{' '}
|
||||
{app?.spec?.destination?.server ===
|
||||
'https://kubernetes.default.svc' ? (
|
||||
<Tooltip
|
||||
data-testid="local-cluster-tooltip"
|
||||
title="This is the local cluster where Argo CD is installed."
|
||||
>
|
||||
<span>(in-cluster) </span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body1" color="textPrimary">
|
||||
Namespace
|
||||
</Typography>
|
||||
|
||||
<AppNamespace app={app} />
|
||||
</Grid>
|
||||
{!isAppHelmChartType(app) && (
|
||||
<Grid item xs={12}>
|
||||
<Typography color="textPrimary">Commit</Typography>
|
||||
{revisionsMap && latestRevision ? (
|
||||
<>
|
||||
<Chip
|
||||
data-testid={`${latestRevision?.revision?.slice(
|
||||
0,
|
||||
5,
|
||||
)}-commit-link`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
const repoUrl = app?.spec?.source?.repoURL ?? '';
|
||||
if (repoUrl.length) {
|
||||
window.open(
|
||||
getCommitUrl(
|
||||
repoUrl,
|
||||
latestRevision?.revision,
|
||||
entity?.metadata?.annotations ?? {},
|
||||
),
|
||||
'_blank',
|
||||
);
|
||||
}
|
||||
}}
|
||||
icon={<GitLabIcon />}
|
||||
color="primary"
|
||||
label={latestRevision?.revision.slice(0, 7)}
|
||||
/>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
className={classes.commitMessage}
|
||||
>
|
||||
{revisionsMap?.[latestRevision?.revision] ? (
|
||||
<Tooltip
|
||||
data-testid={`${latestRevision?.revision?.slice(
|
||||
0,
|
||||
5,
|
||||
)}-commit-message`}
|
||||
title={
|
||||
revisionsMap?.[latestRevision?.revision]?.message
|
||||
}
|
||||
>
|
||||
<span>
|
||||
{revisionsMap?.[latestRevision?.revision]?.message}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Skeleton />
|
||||
)}
|
||||
</Typography>
|
||||
</>
|
||||
) : (
|
||||
<>-</>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body1" color="textPrimary">
|
||||
Deployment
|
||||
</Typography>
|
||||
{appHistory.length >= 1 ? (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Image{' '}
|
||||
<Link
|
||||
href={`https://${app?.status?.summary?.images?.[0]}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{app?.status?.summary?.images?.[0].split('/').pop()}
|
||||
</Link>{' '}
|
||||
deployed {moment(appDeployedAt).local().fromNow()}
|
||||
</Typography>
|
||||
) : (
|
||||
<>-</>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
export default DeploymentLifecycleCard;
|
||||
|
|
@ -0,0 +1,302 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
createStyles,
|
||||
Drawer,
|
||||
Grid,
|
||||
IconButton,
|
||||
Link,
|
||||
makeStyles,
|
||||
Theme,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import Close from '@material-ui/icons/Close';
|
||||
import { Skeleton } from '@material-ui/lab';
|
||||
import GitLabIcon from '@patternfly/react-icons/dist/esm/icons/gitlab-icon';
|
||||
import moment from 'moment';
|
||||
|
||||
import { Application, Revision } from '../../types';
|
||||
import { getCommitUrl, isAppHelmChartType } from '../../utils/utils';
|
||||
import AppNamespace from '../AppStatus/AppNamespace';
|
||||
import StatusHeading from '../AppStatus/StatusHeading';
|
||||
import DeploymentLifecycledHeader from './DeploymentLifecycleHeader';
|
||||
|
||||
interface DeploymentLifecycleDrawerProps {
|
||||
app: Application | undefined;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
revisionsMap: { [key: string]: Revision };
|
||||
}
|
||||
|
||||
const useDrawerStyles = makeStyles<Theme>(theme =>
|
||||
createStyles({
|
||||
icon: {
|
||||
fontSize: 20,
|
||||
},
|
||||
paper: {
|
||||
width: '40%',
|
||||
padding: theme.spacing(2.5),
|
||||
gap: '3%',
|
||||
},
|
||||
header: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'baseline',
|
||||
},
|
||||
commitMessage: {
|
||||
wordBreak: 'break-word',
|
||||
},
|
||||
}),
|
||||
);
|
||||
const DeploymentLifecycleDrawer: React.FC<DeploymentLifecycleDrawerProps> = ({
|
||||
app,
|
||||
isOpen,
|
||||
onClose,
|
||||
revisionsMap,
|
||||
}) => {
|
||||
const appHistory = app?.status?.history ?? [];
|
||||
const latestRevision = appHistory[appHistory.length - 1];
|
||||
const appDeployedAt = latestRevision?.deployedAt;
|
||||
|
||||
const { entity } = useEntity();
|
||||
const classes = useDrawerStyles();
|
||||
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Drawer
|
||||
data-testid={`${app?.metadata?.name}-drawer`}
|
||||
anchor="right"
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
classes={{
|
||||
paper: classes.paper,
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Grid container alignItems="stretch">
|
||||
<Grid item xs={12}>
|
||||
<div className={classes.header}>
|
||||
<Typography variant="h4">
|
||||
<DeploymentLifecycledHeader app={app} />
|
||||
</Typography>
|
||||
|
||||
<IconButton
|
||||
key="dismiss"
|
||||
title="Close the drawer"
|
||||
onClick={onClose}
|
||||
color="inherit"
|
||||
>
|
||||
<Close className={classes.icon} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<StatusHeading app={app} />
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Typography color="textPrimary">Instance</Typography>
|
||||
|
||||
<Typography color="textSecondary" gutterBottom>
|
||||
{app?.metadata?.instance?.name}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body1">Server</Typography>
|
||||
|
||||
<Typography
|
||||
style={{ overflowWrap: 'anywhere' }}
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
gutterBottom
|
||||
>
|
||||
{app?.spec?.destination?.server}{' '}
|
||||
{app?.spec?.destination?.server ===
|
||||
'https://kubernetes.default.svc' ? (
|
||||
<Tooltip title="This is the local cluster where Argo CD is installed.">
|
||||
<span>(in-cluster) </span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography color="textPrimary">Namespace</Typography>
|
||||
|
||||
<AppNamespace app={app} />
|
||||
</Grid>
|
||||
{!isAppHelmChartType(app) && (
|
||||
<Grid item xs={12}>
|
||||
<Typography color="textPrimary">Commit</Typography>
|
||||
{latestRevision ? (
|
||||
<>
|
||||
<Chip
|
||||
data-testid={`${latestRevision?.revision?.slice(
|
||||
0,
|
||||
5,
|
||||
)}-commit-link`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
icon={<GitLabIcon />}
|
||||
color="primary"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
|
||||
const repoUrl = app?.spec?.source?.repoURL ?? '';
|
||||
if (repoUrl) {
|
||||
window.open(
|
||||
isAppHelmChartType(app)
|
||||
? repoUrl
|
||||
: getCommitUrl(
|
||||
repoUrl,
|
||||
latestRevision?.revision,
|
||||
entity?.metadata?.annotations ?? {},
|
||||
),
|
||||
'_blank',
|
||||
);
|
||||
}
|
||||
}}
|
||||
label={latestRevision?.revision.slice(0, 7)}
|
||||
/>
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
className={classes.commitMessage}
|
||||
>
|
||||
{revisionsMap?.[latestRevision?.revision] ? (
|
||||
<>
|
||||
{revisionsMap?.[latestRevision?.revision]?.message} by{' '}
|
||||
{revisionsMap?.[latestRevision?.revision]?.author}
|
||||
</>
|
||||
) : (
|
||||
<Skeleton />
|
||||
)}
|
||||
</Typography>
|
||||
</>
|
||||
) : (
|
||||
<>-</>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
{appHistory.length >= 1 && (
|
||||
<Grid item xs={12}>
|
||||
<Typography color="textPrimary">Latest deployment</Typography>
|
||||
|
||||
<Card elevation={2} style={{ margin: '10px' }}>
|
||||
<CardContent>
|
||||
<Typography color="textPrimary" gutterBottom>
|
||||
Deployment
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
className={classes.commitMessage}
|
||||
>
|
||||
Image{' '}
|
||||
<Link
|
||||
href={`https://${app?.status?.summary?.images?.[0]}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{app?.status?.summary?.images?.[0].split('/').pop()}
|
||||
</Link>
|
||||
<br />
|
||||
{revisionsMap[latestRevision?.revision]?.message}{' '}
|
||||
<Link
|
||||
href={
|
||||
isAppHelmChartType(app)
|
||||
? app?.spec?.source?.repoURL
|
||||
: getCommitUrl(
|
||||
app?.spec?.source?.repoURL ?? '',
|
||||
latestRevision?.revision,
|
||||
entity?.metadata?.annotations ?? {},
|
||||
)
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{latestRevision?.revision.slice(0, 7)}
|
||||
</Link>{' '}
|
||||
deployed {moment(appDeployedAt).local().fromNow()}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
)}
|
||||
{appHistory.length > 1 && (
|
||||
<Grid item xs={12}>
|
||||
<Typography color="textPrimary">Deployment history</Typography>
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
margin: 0,
|
||||
padding: '0',
|
||||
height: '35vh',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
<br />
|
||||
{app?.status?.history
|
||||
?.slice()
|
||||
?.reverse()
|
||||
.slice(1)
|
||||
?.map(dep => {
|
||||
const commitUrl = app?.spec?.source?.repoURL
|
||||
? getCommitUrl(
|
||||
app.spec.source.repoURL,
|
||||
dep?.revision,
|
||||
entity?.metadata?.annotations ?? {},
|
||||
)
|
||||
: null;
|
||||
return (
|
||||
<Card
|
||||
elevation={2}
|
||||
key={dep.id}
|
||||
style={{ margin: '10px' }}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography color="textPrimary" gutterBottom>
|
||||
Deployment
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
className={classes.commitMessage}
|
||||
>
|
||||
{revisionsMap[dep.revision]?.message}{' '}
|
||||
<Link
|
||||
aria-disabled={!!commitUrl}
|
||||
href={commitUrl ?? ''}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{dep.revision.slice(0, 7)}
|
||||
</Link>{' '}
|
||||
deployed {moment(dep.deployedAt).local().fromNow()}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
export default DeploymentLifecycleDrawer;
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
|
||||
import { IconButton } from '@material-ui/core';
|
||||
import ExternalLinkIcon from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon';
|
||||
|
||||
import { useArgocdConfig } from '../../hooks/useArgocdConfig';
|
||||
import { Application } from '../../types';
|
||||
|
||||
const DeploymentLifecycleHeader: React.FC<{ app: Application }> = ({ app }) => {
|
||||
const { instances, baseUrl } = useArgocdConfig();
|
||||
|
||||
const supportsMultipleArgoInstances = !!instances.length;
|
||||
const getBaseUrl = (row: Application): string | undefined => {
|
||||
if (supportsMultipleArgoInstances && !baseUrl) {
|
||||
return instances?.find(
|
||||
value => value?.name === row.metadata?.instance?.name,
|
||||
)?.url;
|
||||
}
|
||||
return baseUrl;
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{app.metadata.name}{' '}
|
||||
<IconButton
|
||||
data-testid={`${app.metadata.name}-link`}
|
||||
color="primary"
|
||||
size="small"
|
||||
target="_blank"
|
||||
href={`${getBaseUrl(app)}/applications/${app.metadata.name}`}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<ExternalLinkIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeploymentLifecycleHeader;
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
import React, { PropsWithChildren } from 'react';
|
||||
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { usePermission } from '@backstage/plugin-permission-react';
|
||||
|
||||
import { createTheme, ThemeProvider } from '@material-ui/core';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import { mockApplication, mockEntity } from '../../../../dev/__data__';
|
||||
import { useArgocdConfig } from '../../../hooks/useArgocdConfig';
|
||||
import DeploymentLifecycle from '../DeploymentLifecycle';
|
||||
|
||||
jest.mock('../../../hooks/useArgocdConfig', () => ({
|
||||
useArgocdConfig: jest.fn(),
|
||||
}));
|
||||
jest.mock('@backstage/plugin-permission-react', () => ({
|
||||
usePermission: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUsePermission = usePermission as jest.MockedFunction<
|
||||
typeof usePermission
|
||||
>;
|
||||
|
||||
jest.mock('@backstage/core-plugin-api', () => ({
|
||||
...jest.requireActual('@backstage/core-plugin-api'),
|
||||
useApi: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@backstage/plugin-catalog-react', () => ({
|
||||
useEntity: () => ({
|
||||
entity: mockEntity,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@backstage/core-components', () => ({
|
||||
...jest.requireActual('@backstage/core-components'),
|
||||
ResponseErrorPanel: ({ error }: { error: Error }) => (
|
||||
<div data-testid="error-panel">{JSON.stringify(error)}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('DeploymentLifecycle', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockUsePermission.mockReturnValue({ loading: false, allowed: true });
|
||||
|
||||
(useArgocdConfig as any).mockReturnValue({
|
||||
baseUrl: 'https://baseurl.com',
|
||||
instances: [{ name: 'main', url: 'https://main-instance-url.com' }],
|
||||
intervalMs: 10000,
|
||||
instanceName: 'main',
|
||||
});
|
||||
|
||||
(useApi as any).mockReturnValue({
|
||||
listApps: async () => {
|
||||
return Promise.resolve({ items: [mockApplication] });
|
||||
},
|
||||
getRevisionDetailsList: async () => {
|
||||
return Promise.resolve({
|
||||
commit: 'commit message',
|
||||
author: 'test-user',
|
||||
date: new Date(),
|
||||
});
|
||||
},
|
||||
});
|
||||
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
it('should render Permission alert if the user does not have view permission', () => {
|
||||
mockUsePermission.mockReturnValue({ loading: false, allowed: false });
|
||||
const { getByTestId } = render(<DeploymentLifecycle />);
|
||||
expect(getByTestId('no-permission-alert')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the loader component', async () => {
|
||||
render(<DeploymentLifecycle />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('argocd-loader')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should render deployment lifecycle component', async () => {
|
||||
render(<DeploymentLifecycle />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('argocd-loader')).not.toBeInTheDocument();
|
||||
|
||||
screen.getByText('Deployment lifecycle');
|
||||
screen.getByTestId('quarkus-app-dev-card');
|
||||
});
|
||||
});
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
type: 'dark',
|
||||
},
|
||||
});
|
||||
|
||||
const Providers = ({ children }: PropsWithChildren) => (
|
||||
<ThemeProvider theme={theme}>{children}</ThemeProvider>
|
||||
);
|
||||
|
||||
test('should render components in dark theme', async () => {
|
||||
render(
|
||||
<Providers>
|
||||
<DeploymentLifecycle />
|
||||
</Providers>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('argocd-loader')).not.toBeInTheDocument();
|
||||
|
||||
screen.getByText('Deployment lifecycle');
|
||||
screen.getByTestId('quarkus-app-dev-card');
|
||||
});
|
||||
});
|
||||
|
||||
test('should catch the error while fetching revision details', async () => {
|
||||
(useApi as any).mockReturnValue({
|
||||
listApps: async () => {
|
||||
return Promise.resolve({ items: [mockApplication] });
|
||||
},
|
||||
getRevisionDetailsList: async () => {
|
||||
return Promise.reject(new Error('500: Internal server error'));
|
||||
},
|
||||
});
|
||||
|
||||
render(<DeploymentLifecycle />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('argocd-loader')).not.toBeInTheDocument();
|
||||
|
||||
screen.getByText('Deployment lifecycle');
|
||||
screen.getByTestId('quarkus-app-dev-card');
|
||||
});
|
||||
});
|
||||
|
||||
test('should catch the error while fetching applications', async () => {
|
||||
(useApi as any).mockReturnValue({
|
||||
listApps: async () => {
|
||||
return Promise.reject(new Error('500: Internal server error'));
|
||||
},
|
||||
getRevisionDetailsList: async () => {
|
||||
return Promise.resolve({
|
||||
commit: 'commit message',
|
||||
author: 'test-user',
|
||||
date: new Date(),
|
||||
});
|
||||
},
|
||||
});
|
||||
render(<DeploymentLifecycle />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('argocd-loader')).not.toBeInTheDocument();
|
||||
screen.getByTestId('error-panel');
|
||||
});
|
||||
});
|
||||
|
||||
test('should not render the component if there are no applications matching the selector', async () => {
|
||||
(useApi as any).mockReturnValue({
|
||||
listApps: async () => {
|
||||
return Promise.resolve({ items: [] });
|
||||
},
|
||||
getRevisionDetailsList: async () => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
});
|
||||
render(<DeploymentLifecycle />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('argocd-loader')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText('Deployment lifecycle'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should open and close the sidebar', async () => {
|
||||
render(<DeploymentLifecycle />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('argocd-loader')).not.toBeInTheDocument();
|
||||
screen.getByText('Deployment lifecycle');
|
||||
|
||||
screen.getByTestId('quarkus-app-dev-card');
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId('quarkus-app-dev-card'));
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByTestId('quarkus-app-dev-drawer');
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Close the drawer' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByTestId('quarkus-app-dev-drawer'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
import React from 'react';
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import { mockApplication, mockEntity } from '../../../../dev/__data__';
|
||||
import { useArgocdConfig } from '../../../hooks/useArgocdConfig';
|
||||
import { Application, Source } from '../../../types';
|
||||
import DeploymentLifecycleCard from '../DeploymentLifecycleCard';
|
||||
|
||||
jest.mock('@backstage/plugin-catalog-react', () => ({
|
||||
useEntity: () => ({
|
||||
...mockEntity,
|
||||
metadata: {
|
||||
...mockEntity.metadata,
|
||||
annotations: {
|
||||
...mockEntity.metadata.annotations,
|
||||
'gitlab.com/source-url': 'https://gitlab.com/testingrepo',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../../hooks/useArgocdConfig', () => ({
|
||||
useArgocdConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('DeploymentLifecylceCard', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
(useArgocdConfig as any).mockReturnValue({
|
||||
baseUrl: '',
|
||||
instances: [{ name: 'main', url: 'https://main-instance-url.com' }],
|
||||
intervalMs: 10000,
|
||||
instanceName: 'main',
|
||||
});
|
||||
});
|
||||
|
||||
test('should not render if the application is not passed', () => {
|
||||
render(
|
||||
<DeploymentLifecycleCard
|
||||
app={null as unknown as Application}
|
||||
revisionsMap={{}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('quarkus-app-dev-card'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
test('should render if the application card', () => {
|
||||
render(<DeploymentLifecycleCard app={mockApplication} revisionsMap={{}} />);
|
||||
expect(screen.getByTestId('quarkus-app-dev-card')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('application header should link to external link', () => {
|
||||
render(<DeploymentLifecycleCard app={mockApplication} revisionsMap={{}} />);
|
||||
const link = screen.getByTestId('quarkus-app-dev-link');
|
||||
fireEvent.click(link);
|
||||
|
||||
expect(link).toHaveAttribute(
|
||||
'href',
|
||||
'https://main-instance-url.com/applications/quarkus-app-dev',
|
||||
);
|
||||
});
|
||||
|
||||
test('should render incluster tooltip', () => {
|
||||
render(<DeploymentLifecycleCard app={mockApplication} revisionsMap={{}} />);
|
||||
|
||||
fireEvent.mouseDown(screen.getByText('(in-cluster)'));
|
||||
|
||||
expect(screen.getByTestId('local-cluster-tooltip')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render remote cluster url', () => {
|
||||
const remoteApplication: Application = {
|
||||
...mockApplication,
|
||||
spec: {
|
||||
...mockApplication.spec,
|
||||
destination: {
|
||||
server: 'https://remote-url.com',
|
||||
namespace: 'remote-ns',
|
||||
},
|
||||
},
|
||||
};
|
||||
render(
|
||||
<DeploymentLifecycleCard app={remoteApplication} revisionsMap={{}} />,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('(in-cluster)')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('local-cluster-tooltip'),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
screen.getByText('https://remote-url.com');
|
||||
});
|
||||
|
||||
test('should not open a new windown if the missing git url', () => {
|
||||
const remoteApplication: Application = {
|
||||
...mockApplication,
|
||||
spec: {
|
||||
...mockApplication.spec,
|
||||
source: undefined as unknown as Source,
|
||||
},
|
||||
};
|
||||
global.open = jest.fn();
|
||||
|
||||
render(
|
||||
<DeploymentLifecycleCard app={remoteApplication} revisionsMap={{}} />,
|
||||
);
|
||||
const commitLink = screen.getByTestId('90f97-commit-link');
|
||||
fireEvent.click(commitLink);
|
||||
|
||||
expect(global.open).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('application card should contain commit link', () => {
|
||||
(useArgocdConfig as any).mockReturnValue({
|
||||
baseUrl: 'https://baseUrl.com',
|
||||
instances: [{ name: 'main', url: 'https://main-instance-url.com' }],
|
||||
intervalMs: 10000,
|
||||
instanceName: 'main',
|
||||
});
|
||||
|
||||
global.open = jest.fn();
|
||||
|
||||
render(
|
||||
<DeploymentLifecycleCard
|
||||
app={mockApplication}
|
||||
revisionsMap={{
|
||||
'90f9758b7033a4bbb7c33a35ee474d61091644bc': {
|
||||
author: 'test user',
|
||||
message: 'commit message',
|
||||
date: new Date(),
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const commitLink = screen.getByTestId('90f97-commit-link');
|
||||
fireEvent.click(commitLink);
|
||||
|
||||
expect(global.open).toHaveBeenCalled();
|
||||
expect(global.open).toHaveBeenCalledWith(
|
||||
'https://gitlab-gitlab.apps.cluster.test.com/development/quarkus-app-gitops',
|
||||
'_blank',
|
||||
);
|
||||
screen.getByText('commit message');
|
||||
});
|
||||
});
|
||||
|
||||
test('application card should not contain commit section for helm based applications', () => {
|
||||
(useArgocdConfig as any).mockReturnValue({
|
||||
baseUrl: 'https://baseUrl.com',
|
||||
instances: [{ name: 'main', url: 'https://main-instance-url.com' }],
|
||||
intervalMs: 10000,
|
||||
instanceName: 'main',
|
||||
});
|
||||
|
||||
const helmApplication = {
|
||||
...mockApplication,
|
||||
spec: {
|
||||
...mockApplication.spec,
|
||||
source: { ...mockApplication.spec.source, chart: 'redhat-charts' },
|
||||
},
|
||||
};
|
||||
|
||||
render(<DeploymentLifecycleCard app={helmApplication} revisionsMap={{}} />);
|
||||
|
||||
const commitLink = screen.queryByText('Commit');
|
||||
expect(commitLink).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
import React from 'react';
|
||||
|
||||
import { configApiRef } from '@backstage/core-plugin-api';
|
||||
import { MockConfigApi, TestApiProvider } from '@backstage/test-utils';
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import { mockApplication, mockEntity } from '../../../../dev/__data__';
|
||||
import { Application, Source } from '../../../types';
|
||||
import DeploymentLifecycleDrawer from '../DeploymentLifecycleDrawer';
|
||||
|
||||
jest.mock('@backstage/plugin-catalog-react', () => ({
|
||||
useEntity: () => ({
|
||||
...mockEntity,
|
||||
metadata: {
|
||||
...mockEntity.metadata,
|
||||
annotations: {
|
||||
...mockEntity.metadata.annotations,
|
||||
'gitlab.com/source-url': 'https://gitlab.com/testingrepo',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('DeploymentLifecycleDrawer', () => {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
configApiRef,
|
||||
new MockConfigApi({
|
||||
argocd: {
|
||||
appLocatorMethods: [
|
||||
{
|
||||
instances: [
|
||||
{
|
||||
name: 'main',
|
||||
url: 'https://test.com',
|
||||
},
|
||||
],
|
||||
type: 'config',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
);
|
||||
};
|
||||
test('should not render the application drawer component', () => {
|
||||
render(
|
||||
<DeploymentLifecycleDrawer
|
||||
app={null as unknown as Application}
|
||||
isOpen
|
||||
onClose={() => jest.fn()}
|
||||
revisionsMap={{}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('quarkus-app-dev-drawer'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the application drawer component', () => {
|
||||
render(
|
||||
<DeploymentLifecycleDrawer
|
||||
app={mockApplication}
|
||||
isOpen
|
||||
onClose={() => jest.fn()}
|
||||
revisionsMap={{}}
|
||||
/>,
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('quarkus-app-dev-drawer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not render the commit section for helm based applications in drawer component', () => {
|
||||
const helmApplication = {
|
||||
...mockApplication,
|
||||
spec: {
|
||||
...mockApplication.spec,
|
||||
source: { ...mockApplication.spec.source, chart: 'redhat-charts' },
|
||||
},
|
||||
};
|
||||
render(
|
||||
<DeploymentLifecycleDrawer
|
||||
app={helmApplication}
|
||||
isOpen
|
||||
onClose={() => jest.fn()}
|
||||
revisionsMap={{}}
|
||||
/>,
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
const commitLink = screen.queryByText('Commit');
|
||||
expect(commitLink).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the commit link in drawer component', () => {
|
||||
global.open = jest.fn();
|
||||
|
||||
render(
|
||||
<DeploymentLifecycleDrawer
|
||||
app={mockApplication}
|
||||
isOpen
|
||||
onClose={() => jest.fn()}
|
||||
revisionsMap={{
|
||||
'90f9758b7033a4bbb7c33a35ee474d61091644bc': {
|
||||
author: 'test user',
|
||||
message: 'commit message',
|
||||
date: new Date(),
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
{ wrapper },
|
||||
);
|
||||
const commitLink = screen.getByTestId('90f97-commit-link');
|
||||
fireEvent.click(commitLink);
|
||||
|
||||
expect(global.open).toHaveBeenCalled();
|
||||
expect(global.open).toHaveBeenCalledWith(
|
||||
'https://gitlab-gitlab.apps.cluster.test.com/development/quarkus-app-gitops',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
|
||||
test('should not open a new windown if the missing git url', () => {
|
||||
const remoteApplication: Application = {
|
||||
...mockApplication,
|
||||
spec: {
|
||||
...mockApplication.spec,
|
||||
source: undefined as unknown as Source,
|
||||
},
|
||||
};
|
||||
global.open = jest.fn();
|
||||
|
||||
render(
|
||||
<DeploymentLifecycleDrawer
|
||||
isOpen
|
||||
onClose={jest.fn()}
|
||||
app={remoteApplication}
|
||||
revisionsMap={{}}
|
||||
/>,
|
||||
{ wrapper },
|
||||
);
|
||||
const commitLink = screen.getByTestId('90f97-commit-link');
|
||||
fireEvent.click(commitLink);
|
||||
|
||||
expect(global.open).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('should render remote cluster url', () => {
|
||||
const remoteApplication: Application = {
|
||||
...mockApplication,
|
||||
spec: {
|
||||
...mockApplication.spec,
|
||||
destination: {
|
||||
server: 'https://remote-url.com',
|
||||
namespace: 'remote-ns',
|
||||
},
|
||||
},
|
||||
};
|
||||
render(
|
||||
<DeploymentLifecycleDrawer
|
||||
isOpen
|
||||
onClose={jest.fn()}
|
||||
app={remoteApplication}
|
||||
revisionsMap={{}}
|
||||
/>,
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(screen.queryByText('(in-cluster)')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('local-cluster-tooltip'),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
screen.getByText('https://remote-url.com');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import React from 'react';
|
||||
|
||||
import { configApiRef } from '@backstage/core-plugin-api';
|
||||
import { MockConfigApi, TestApiProvider } from '@backstage/test-utils';
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import { mockApplication } from '../../../../dev/__data__';
|
||||
import DeploymentLifecycleHeader from '../DeploymentLifecycleHeader';
|
||||
|
||||
describe('DeploymentLifecycleCardHeader', () => {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
configApiRef,
|
||||
new MockConfigApi({
|
||||
argocd: {
|
||||
appLocatorMethods: [
|
||||
{
|
||||
instances: [
|
||||
{
|
||||
name: 'main',
|
||||
url: 'https://test.com',
|
||||
},
|
||||
],
|
||||
type: 'config',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
test('should render the deployment lifecylce appliction header', () => {
|
||||
render(<DeploymentLifecycleHeader app={mockApplication} />, { wrapper });
|
||||
|
||||
expect(screen.queryByText('quarkus-app-dev')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('quarkus-app-dev-link')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the deployment lifecylce appliction header link with instance url', () => {
|
||||
render(<DeploymentLifecycleHeader app={mockApplication} />, { wrapper });
|
||||
const link = screen.queryByTestId(
|
||||
'quarkus-app-dev-link',
|
||||
) as HTMLLinkElement;
|
||||
|
||||
fireEvent.click(link);
|
||||
|
||||
expect(link).toHaveAttribute(
|
||||
'href',
|
||||
'https://test.com/applications/quarkus-app-dev',
|
||||
);
|
||||
});
|
||||
|
||||
test('should render the deployment lifecylce appliction header with base url', () => {
|
||||
const apiProviderWrapper = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
configApiRef,
|
||||
new MockConfigApi({
|
||||
argocd: {
|
||||
baseUrl: 'https://baseurl.com',
|
||||
appLocatorMethods: [],
|
||||
},
|
||||
}),
|
||||
],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
render(<DeploymentLifecycleHeader app={mockApplication} />, {
|
||||
wrapper: apiProviderWrapper,
|
||||
});
|
||||
const link = screen.queryByTestId(
|
||||
'quarkus-app-dev-link',
|
||||
) as HTMLLinkElement;
|
||||
|
||||
expect(link).toHaveAttribute(
|
||||
'href',
|
||||
'https://baseurl.com/applications/quarkus-app-dev',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as DeploymentLifecycle } from './DeploymentLifecycle';
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Table, TableColumn } from '@backstage/core-components';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
|
||||
import { IconButton, Link } from '@material-ui/core';
|
||||
import ExternalLinkIcon from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon';
|
||||
import moment from 'moment';
|
||||
|
||||
import { useApplications } from '../../hooks/useApplications';
|
||||
import { useArgocdConfig } from '../../hooks/useArgocdConfig';
|
||||
import { useArgocdViewPermission } from '../../hooks/useArgocdViewPermission';
|
||||
import { Application, HealthStatus, SyncStatuses } from '../../types';
|
||||
import {
|
||||
getArgoCdAppConfig,
|
||||
getCommitUrl,
|
||||
getInstanceName,
|
||||
isAppHelmChartType,
|
||||
} from '../../utils/utils';
|
||||
import AppSyncStatus from '../AppStatus/AppSyncStatus';
|
||||
import { AppHealthIcon } from '../AppStatus/StatusIcons';
|
||||
|
||||
const DeploymentSummary = () => {
|
||||
const { entity } = useEntity();
|
||||
|
||||
const { baseUrl, instances, intervalMs } = useArgocdConfig();
|
||||
const instanceName = getInstanceName(entity) || instances?.[0]?.name;
|
||||
|
||||
const { appSelector, appName, projectName, appNamespace } =
|
||||
getArgoCdAppConfig({ entity });
|
||||
|
||||
const { apps, loading, error } = useApplications({
|
||||
instanceName,
|
||||
intervalMs,
|
||||
appSelector,
|
||||
projectName,
|
||||
appName,
|
||||
appNamespace,
|
||||
});
|
||||
|
||||
const hasArgocdViewAccess = useArgocdViewPermission();
|
||||
|
||||
const supportsMultipleArgoInstances = !!instances.length;
|
||||
const getBaseUrl = (row: any): string | undefined => {
|
||||
if (supportsMultipleArgoInstances && !baseUrl) {
|
||||
return instances?.find(
|
||||
value => value?.name === row.metadata?.instance?.name,
|
||||
)?.url;
|
||||
}
|
||||
return baseUrl;
|
||||
};
|
||||
|
||||
const columns: TableColumn<Application>[] = [
|
||||
{
|
||||
title: 'ArgoCD App',
|
||||
field: 'name',
|
||||
render: (row: Application): React.ReactNode =>
|
||||
getBaseUrl(row) ? (
|
||||
<Link
|
||||
href={`${getBaseUrl(row)}/applications/${row?.metadata?.name}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{row.metadata.name}{' '}
|
||||
<IconButton color="primary" size="small">
|
||||
<ExternalLinkIcon />
|
||||
</IconButton>
|
||||
</Link>
|
||||
) : (
|
||||
row.metadata.name
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Namespace',
|
||||
field: 'namespace',
|
||||
render: (row: Application): React.ReactNode => {
|
||||
return <>{row.spec.destination.namespace}</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Instance',
|
||||
field: 'instance',
|
||||
render: (row: Application): React.ReactNode => {
|
||||
return <>{row.metadata?.instance?.name || instanceName}</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Server',
|
||||
field: 'server',
|
||||
render: (row: Application): React.ReactNode => {
|
||||
return <>{row.spec.destination.server}</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Revision',
|
||||
field: 'revision',
|
||||
render: (row: Application): React.ReactNode => {
|
||||
const historyList = row.status?.history ?? [];
|
||||
const latestRev = historyList[historyList.length - 1];
|
||||
const repoUrl = row?.spec?.source?.repoURL;
|
||||
const commitUrl = isAppHelmChartType(row)
|
||||
? repoUrl
|
||||
: getCommitUrl(
|
||||
repoUrl,
|
||||
latestRev?.revision,
|
||||
entity?.metadata?.annotations || {},
|
||||
);
|
||||
return (
|
||||
<Link href={commitUrl} target="_blank" rel="noopener">
|
||||
{latestRev?.revision?.substring(0, 7) ?? '-'}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'test',
|
||||
title: 'Last deployed',
|
||||
field: 'lastdeployed',
|
||||
customSort: (a: Application, b: Application) => {
|
||||
const bHistory = b?.status?.history ?? [];
|
||||
const bDeployedAt = bHistory?.[bHistory.length - 1]?.deployedAt;
|
||||
|
||||
const aHistory = a?.status?.history ?? [];
|
||||
const aDeployedAt = aHistory?.[aHistory.length - 1]?.deployedAt;
|
||||
|
||||
return moment(aDeployedAt).diff(moment(bDeployedAt));
|
||||
},
|
||||
render: (row: Application): React.ReactNode => {
|
||||
const history = row?.status?.history ?? [];
|
||||
const finishedAt = history[history.length - 1]?.deployedAt;
|
||||
return (
|
||||
<>
|
||||
{finishedAt
|
||||
? moment(finishedAt).local().format('D/MM/YYYY, H:mm:ss')
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Sync status',
|
||||
field: 'syncstatus',
|
||||
customSort: (a: Application, b: Application): number => {
|
||||
const syncStatusOrder: string[] = Object.values(SyncStatuses);
|
||||
return (
|
||||
syncStatusOrder.indexOf(a?.status?.sync?.status) -
|
||||
syncStatusOrder.indexOf(b?.status?.sync?.status)
|
||||
);
|
||||
},
|
||||
render: (row: Application): React.ReactNode => (
|
||||
<AppSyncStatus app={row} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Health status',
|
||||
field: 'healthstatus',
|
||||
customSort: (a: Application, b: Application): number => {
|
||||
const healthStatusOrder: string[] = Object.values(HealthStatus);
|
||||
return (
|
||||
healthStatusOrder.indexOf(a?.status?.health?.status) -
|
||||
healthStatusOrder.indexOf(b?.status?.health?.status)
|
||||
);
|
||||
},
|
||||
render: (row: Application): React.ReactNode => (
|
||||
<>
|
||||
<AppHealthIcon status={row.status.health.status as HealthStatus} />{' '}
|
||||
{row?.status?.health?.status}
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return !error && hasArgocdViewAccess ? (
|
||||
<Table
|
||||
title="Deployment summary"
|
||||
options={{
|
||||
paging: true,
|
||||
search: false,
|
||||
draggable: false,
|
||||
padding: 'dense',
|
||||
}}
|
||||
isLoading={loading}
|
||||
data={apps}
|
||||
columns={columns}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
export default DeploymentSummary;
|
||||
|
|
@ -0,0 +1,344 @@
|
|||
import React from 'react';
|
||||
|
||||
import { usePermission } from '@backstage/plugin-permission-react';
|
||||
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
|
||||
import { mockApplication, mockEntity } from '../../../../dev/__data__';
|
||||
import { useApplications } from '../../../hooks/useApplications';
|
||||
import { useArgocdConfig } from '../../../hooks/useArgocdConfig';
|
||||
import { Application, History } from '../../../types';
|
||||
import DeploymentSummary from '../DeploymentSummary';
|
||||
|
||||
jest.mock('../../../hooks/useArgocdConfig', () => ({
|
||||
useArgocdConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../hooks/useApplications', () => ({
|
||||
useApplications: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@backstage/plugin-permission-react', () => ({
|
||||
usePermission: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUsePermission = usePermission as jest.MockedFunction<
|
||||
typeof usePermission
|
||||
>;
|
||||
|
||||
jest.mock('@backstage/plugin-catalog-react', () => ({
|
||||
useEntity: () => ({
|
||||
entity: mockEntity,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('DeploymentSummary', () => {
|
||||
beforeEach(() => {
|
||||
(useApplications as any).mockReturnValue({
|
||||
apps: [mockApplication],
|
||||
loading: false,
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
mockUsePermission.mockReturnValue({ loading: false, allowed: true });
|
||||
|
||||
(useArgocdConfig as any).mockReturnValue({
|
||||
baseUrl: '',
|
||||
instances: [{ name: 'main', url: 'https://main-instance-url.com' }],
|
||||
intervalMs: 10000,
|
||||
instanceName: 'main',
|
||||
});
|
||||
});
|
||||
|
||||
test('should render loading indicator', async () => {
|
||||
(useApplications as any).mockReturnValue({
|
||||
apps: [],
|
||||
loading: true,
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
render(<DeploymentSummary />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should render empty table incase of no applications', async () => {
|
||||
(useApplications as any).mockReturnValue({
|
||||
apps: [],
|
||||
loading: false,
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
(useArgocdConfig as any).mockReturnValue({
|
||||
baseUrl: 'https://baseurl.com',
|
||||
instances: [{ name: 'test', url: 'https://main-instance-url.com' }],
|
||||
intervalMs: 10000,
|
||||
instanceName: 'main',
|
||||
});
|
||||
|
||||
render(<DeploymentSummary />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('No records to display')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
test('should not render deployment summary table when the user does not have view permission', async () => {
|
||||
mockUsePermission.mockReturnValue({ loading: false, allowed: false });
|
||||
|
||||
render(<DeploymentSummary />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Deployment summary')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should not render deployment summary table incase of error', async () => {
|
||||
(useApplications as any).mockReturnValue({
|
||||
apps: [],
|
||||
loading: false,
|
||||
error: new Error('500: Internal server error'),
|
||||
});
|
||||
|
||||
render(<DeploymentSummary />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Deployment summary')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should render deployment summary', async () => {
|
||||
(useArgocdConfig as any).mockReturnValue({
|
||||
instances: [{ name: 'test', url: 'https://main-instance-url.com' }],
|
||||
intervalMs: 10000,
|
||||
instanceName: 'test',
|
||||
});
|
||||
|
||||
render(<DeploymentSummary />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByText('quarkus-app-dev')).toBeDefined();
|
||||
expect(screen.queryByText('Healthy')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Synced')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should link the application to instance url', async () => {
|
||||
render(<DeploymentSummary />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('link', { name: 'quarkus-app-dev' }),
|
||||
).toHaveAttribute(
|
||||
'href',
|
||||
'https://main-instance-url.com/applications/quarkus-app-dev',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should link the application to the base argocd url', async () => {
|
||||
(useArgocdConfig as any).mockReturnValue({
|
||||
baseUrl: 'https://baseurl.com',
|
||||
instances: [],
|
||||
intervalMs: 10000,
|
||||
instanceName: 'main',
|
||||
});
|
||||
|
||||
render(<DeploymentSummary />);
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText('Healthy');
|
||||
screen.getByText('Synced');
|
||||
|
||||
expect(
|
||||
screen.getByRole('link', { name: 'quarkus-app-dev' }),
|
||||
).toHaveAttribute(
|
||||
'href',
|
||||
'https://baseurl.com/applications/quarkus-app-dev',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should sort by last deployment time', async () => {
|
||||
const mockApplicationTwo: Application = {
|
||||
...mockApplication,
|
||||
status: {
|
||||
...mockApplication.status,
|
||||
history: [
|
||||
{
|
||||
...(mockApplication?.status?.history?.[0] as History),
|
||||
revision: '12345',
|
||||
},
|
||||
{
|
||||
...(mockApplication?.status?.history?.[1] as History),
|
||||
revision: 'abcde',
|
||||
deployedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
(useApplications as any).mockReturnValue({
|
||||
apps: [mockApplication, mockApplicationTwo],
|
||||
loading: false,
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
render(<DeploymentSummary />);
|
||||
const lastDeployedHeader = screen.getByRole('button', {
|
||||
name: 'Last deployed',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const tableBody = screen.queryAllByRole('rowgroup')[1];
|
||||
const firstRow = within(tableBody).queryAllByRole('row')[0];
|
||||
|
||||
within(firstRow).getByText('90f9758');
|
||||
});
|
||||
await fireEvent.click(lastDeployedHeader);
|
||||
// miui table requires two clicks to start sorting
|
||||
await fireEvent.click(lastDeployedHeader);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
|
||||
|
||||
const tableBody = screen.queryAllByRole('rowgroup')[1];
|
||||
const firstRow = within(tableBody).queryAllByRole('row')[0];
|
||||
|
||||
within(firstRow).getByText('abcde');
|
||||
});
|
||||
});
|
||||
|
||||
test('should sort by last deployment time even if the history is missing for some applications', async () => {
|
||||
const mockApplicationTwo: Application = {
|
||||
...mockApplication,
|
||||
status: {
|
||||
...mockApplication.status,
|
||||
history: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
(useApplications as any).mockReturnValue({
|
||||
apps: [mockApplication, mockApplicationTwo],
|
||||
loading: false,
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
render(<DeploymentSummary />);
|
||||
|
||||
const lastDeployedHeader = screen.getByRole('button', {
|
||||
name: 'Last deployed',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const tableBody = screen.queryAllByRole('rowgroup')[1];
|
||||
const firstRow = within(tableBody).queryAllByRole('row')[0];
|
||||
|
||||
expect(within(firstRow).queryByText('90f9758')).toBeInTheDocument();
|
||||
});
|
||||
await fireEvent.click(lastDeployedHeader);
|
||||
// miui table requires two clicks to start sorting
|
||||
await fireEvent.click(lastDeployedHeader);
|
||||
|
||||
await waitFor(() => {
|
||||
const tableBody = screen.queryAllByRole('rowgroup')[1];
|
||||
const firstRow = within(tableBody).queryAllByRole('row')[0];
|
||||
|
||||
expect(within(firstRow).getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should sort by application sync status', async () => {
|
||||
const mockApplicationTwo: Application = {
|
||||
...mockApplication,
|
||||
status: {
|
||||
...mockApplication.status,
|
||||
sync: {
|
||||
status: 'OutOfSync',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
(useApplications as any).mockReturnValue({
|
||||
apps: [mockApplication, mockApplicationTwo],
|
||||
loading: false,
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
render(<DeploymentSummary />);
|
||||
const syncStatusHeader = screen.getByRole('button', {
|
||||
name: 'Sync status',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const tableBody = screen.queryAllByRole('rowgroup')[1];
|
||||
const firstRow = within(tableBody).queryAllByRole('row')[0];
|
||||
|
||||
within(firstRow).getByText('Synced');
|
||||
});
|
||||
await fireEvent.click(syncStatusHeader);
|
||||
// miui table requires two clicks to start sorting
|
||||
await fireEvent.click(syncStatusHeader);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
|
||||
|
||||
const tableBody = screen.queryAllByRole('rowgroup')[1];
|
||||
const firstRow = within(tableBody).queryAllByRole('row')[0];
|
||||
|
||||
expect(within(firstRow).getByText('OutOfSync')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should sort by application health status', async () => {
|
||||
const mockApplicationTwo: Application = {
|
||||
...mockApplication,
|
||||
status: {
|
||||
...mockApplication.status,
|
||||
health: {
|
||||
status: 'Degraded',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
(useApplications as any).mockReturnValue({
|
||||
apps: [mockApplication, mockApplicationTwo],
|
||||
loading: false,
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
render(<DeploymentSummary />);
|
||||
const healthStatusHeader = screen.getByRole('button', {
|
||||
name: 'Health status',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const tableBody = screen.queryAllByRole('rowgroup')[1];
|
||||
const firstRow = within(tableBody).queryAllByRole('row')[0];
|
||||
|
||||
expect(within(firstRow).queryByText('Healthy')).toBeInTheDocument();
|
||||
});
|
||||
await fireEvent.click(healthStatusHeader);
|
||||
// miui table requires two clicks to start sorting
|
||||
await fireEvent.click(healthStatusHeader);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
|
||||
|
||||
const tableBody = screen.queryAllByRole('rowgroup')[1];
|
||||
const firstRow = within(tableBody).queryAllByRole('row')[0];
|
||||
|
||||
expect(within(firstRow).getByText('Degraded')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as DeploymentSummary } from '../DeploymentSummary/DeploymentSummary';
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
import { useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
|
||||
import { mockApplication } from '../../../dev/__data__';
|
||||
import { useApplications } from '../useApplications';
|
||||
|
||||
jest.mock('@backstage/core-plugin-api', () => ({
|
||||
...jest.requireActual('@backstage/core-plugin-api'),
|
||||
useApi: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useApplications', () => {
|
||||
beforeEach(() => {
|
||||
(useApi as any).mockReturnValue({
|
||||
getApplication: async () => {
|
||||
return Promise.resolve({ items: [mockApplication] });
|
||||
},
|
||||
listApps: async () => {
|
||||
return Promise.resolve({ items: [mockApplication] });
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should return empty if appselector is not passed', async () => {
|
||||
(useApi as any).mockReturnValue({
|
||||
listApps: async () => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
});
|
||||
const { result } = renderHook(() =>
|
||||
useApplications({
|
||||
instanceName: 'main',
|
||||
appSelector: null as unknown as string,
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(true);
|
||||
expect(result.current.apps).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('should return empty if no applicaitons are available', async () => {
|
||||
(useApi as any).mockReturnValue({
|
||||
listApps: async () => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
});
|
||||
const { result } = renderHook(() =>
|
||||
useApplications({
|
||||
instanceName: 'main',
|
||||
intervalMs: 10000,
|
||||
appSelector: 'rht.gitops.com/quarkus-app-bootstrap',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.apps).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('should return the applications and loading state', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useApplications({
|
||||
instanceName: 'main',
|
||||
intervalMs: 10000,
|
||||
appSelector: 'rht.gitops.com/quarkus-app-bootstrap',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.apps).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('should return the applications and loading state when the app selector updates', async () => {
|
||||
(useApi as any).mockReturnValue({
|
||||
listApps: async () => {
|
||||
return Promise.resolve({ items: [mockApplication] });
|
||||
},
|
||||
});
|
||||
|
||||
const { result, rerender } = renderHook(prop => useApplications(prop), {
|
||||
initialProps: {
|
||||
instanceName: 'main',
|
||||
intervalMs: 10000,
|
||||
appSelector: 'rht.gitops.com/quarkus-app-test',
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.apps).toHaveLength(1);
|
||||
});
|
||||
|
||||
rerender({
|
||||
instanceName: 'main',
|
||||
intervalMs: 10000,
|
||||
appSelector: 'rht.gitops.com/quarkus-app-bootstrap',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(true);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.apps).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('should invoke listApps method when appSelector is passed', async () => {
|
||||
const mockListApps = jest.fn();
|
||||
const mockgetApplication = jest.fn();
|
||||
|
||||
(useApi as any).mockReturnValue({
|
||||
getApplication: mockgetApplication,
|
||||
listApps: mockListApps,
|
||||
});
|
||||
|
||||
renderHook(() =>
|
||||
useApplications({
|
||||
instanceName: 'main',
|
||||
intervalMs: 10000,
|
||||
appSelector: 'rht.gitops.com/quarkus-app-bootstrap',
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockListApps).toHaveBeenCalled();
|
||||
expect(mockgetApplication).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test('should invoke getApplication method if appName is passed', async () => {
|
||||
const mockListApps = jest.fn();
|
||||
const mockgetApplication = jest.fn();
|
||||
(useApi as any).mockReturnValue({
|
||||
getApplication: mockgetApplication,
|
||||
listApps: mockListApps,
|
||||
});
|
||||
|
||||
renderHook(() =>
|
||||
useApplications({
|
||||
instanceName: 'main',
|
||||
intervalMs: 10000,
|
||||
appSelector: null as unknown as string,
|
||||
appName: 'quarkus-app-bootstrap',
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockgetApplication).toHaveBeenCalled();
|
||||
expect(mockListApps).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test('should return single application and loading state when the appName is passed', async () => {
|
||||
(useApi as any).mockReturnValue({
|
||||
getApplication: async () => {
|
||||
return Promise.resolve({ items: [mockApplication] });
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(prop => useApplications(prop), {
|
||||
initialProps: {
|
||||
instanceName: 'main',
|
||||
intervalMs: 10000,
|
||||
appSelector: null as unknown as string,
|
||||
appName: 'quarkus-app-test',
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(true);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.apps).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('should return the applications and loading state when the app name updates', async () => {
|
||||
(useApi as any).mockReturnValue({
|
||||
getApplication: async () => {
|
||||
return Promise.resolve({ items: [mockApplication] });
|
||||
},
|
||||
});
|
||||
|
||||
const { result, rerender } = renderHook(prop => useApplications(prop), {
|
||||
initialProps: {
|
||||
instanceName: 'main',
|
||||
intervalMs: 10000,
|
||||
appSelector: null as unknown as string,
|
||||
appName: 'quarkus-app',
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.apps).toHaveLength(1);
|
||||
});
|
||||
|
||||
rerender({
|
||||
instanceName: 'main',
|
||||
intervalMs: 10000,
|
||||
appSelector: null as unknown as string,
|
||||
appName: 'quarkus-test-app',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(true);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.apps).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
import React from 'react';
|
||||
|
||||
import { configApiRef } from '@backstage/core-plugin-api';
|
||||
import { MockConfigApi, TestApiProvider } from '@backstage/test-utils';
|
||||
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useArgocdConfig } from '../useArgocdConfig';
|
||||
|
||||
describe('useArgocdConfig', () => {
|
||||
it('should return default instance and refreshInterval', () => {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
configApiRef,
|
||||
new MockConfigApi({
|
||||
argocd: {
|
||||
appLocatorMethods: [],
|
||||
},
|
||||
}),
|
||||
],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useArgocdConfig(), { wrapper });
|
||||
|
||||
expect(result.current.instances).toHaveLength(0);
|
||||
expect(result.current.intervalMs).toBe(10000);
|
||||
expect(result.current.baseUrl).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return default name and url', () => {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
configApiRef,
|
||||
new MockConfigApi({
|
||||
argocd: {
|
||||
appLocatorMethods: [
|
||||
{
|
||||
instances: [{}],
|
||||
type: 'config',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useArgocdConfig(), { wrapper });
|
||||
|
||||
expect(result.current.instances).toHaveLength(1);
|
||||
expect(result.current.instances[0].name).toBe('');
|
||||
expect(result.current.instances[0].url).toBe('');
|
||||
});
|
||||
|
||||
it('should return base url', () => {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
configApiRef,
|
||||
new MockConfigApi({
|
||||
argocd: {
|
||||
baseUrl: 'https://argo-base-url.com',
|
||||
appLocatorMethods: [
|
||||
{
|
||||
instances: [{}],
|
||||
type: 'config',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useArgocdConfig(), { wrapper });
|
||||
|
||||
expect(result.current.baseUrl).toBe('https://argo-base-url.com');
|
||||
});
|
||||
|
||||
it('should return configured instance and refreshInterval', () => {
|
||||
const mockConfig = new MockConfigApi({
|
||||
argocd: {
|
||||
refreshInterval: 50000,
|
||||
appLocatorMethods: [
|
||||
{
|
||||
instances: [
|
||||
{
|
||||
name: 'test',
|
||||
url: 'https://test.com',
|
||||
},
|
||||
],
|
||||
type: 'config',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<TestApiProvider apis={[[configApiRef, mockConfig]]}>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useArgocdConfig(), { wrapper });
|
||||
|
||||
expect(result.current.instances).toHaveLength(1);
|
||||
expect(result.current.instances[0].url).toBe('https://test.com');
|
||||
expect(result.current.intervalMs).toBe(50000);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import * as React from 'react';
|
||||
import { useAsyncRetry, useInterval } from 'react-use';
|
||||
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
import { argoCDApiRef } from '../api';
|
||||
import { Application } from '../types';
|
||||
|
||||
interface AppOptions {
|
||||
instanceName: string;
|
||||
appSelector: string;
|
||||
intervalMs?: number;
|
||||
projectName?: string;
|
||||
appName?: string;
|
||||
appNamespace?: string;
|
||||
}
|
||||
|
||||
export const useApplications = ({
|
||||
appName,
|
||||
appNamespace,
|
||||
instanceName,
|
||||
appSelector,
|
||||
projectName,
|
||||
intervalMs = 10000,
|
||||
}: AppOptions): {
|
||||
apps: Application[];
|
||||
error: Error | undefined;
|
||||
loading: boolean;
|
||||
} => {
|
||||
const [loadingData, setLoadingData] = React.useState<boolean>(true);
|
||||
const [, setAppSelector] = React.useState<string>(appSelector ?? '');
|
||||
const [, setAppName] = React.useState<string | undefined>(appName ?? '');
|
||||
const [apps, setApps] = React.useState<Application[]>([]);
|
||||
|
||||
const api = useApi(argoCDApiRef);
|
||||
|
||||
const getApplications = React.useCallback(async () => {
|
||||
return await api
|
||||
.listApps({
|
||||
url: `/argoInstance/${instanceName}`,
|
||||
appSelector,
|
||||
projectName,
|
||||
})
|
||||
.then(applications => setApps(applications?.items ?? []));
|
||||
}, [api, appSelector, instanceName, projectName]);
|
||||
|
||||
const getApplication = React.useCallback(async () => {
|
||||
return await api
|
||||
.getApplication({
|
||||
url: `/argoInstance/${instanceName}`,
|
||||
appName: appName as string,
|
||||
appNamespace,
|
||||
})
|
||||
.then(application => setApps([application]));
|
||||
}, [api, appName, appNamespace, instanceName]);
|
||||
|
||||
const { error, loading, retry } = useAsyncRetry(async () => {
|
||||
if (appName) {
|
||||
return await getApplication();
|
||||
}
|
||||
return await getApplications();
|
||||
}, [getApplications, getApplication]);
|
||||
|
||||
useInterval(() => retry(), intervalMs);
|
||||
|
||||
React.useEffect(() => {
|
||||
let mounted = true;
|
||||
if (!loading && mounted) {
|
||||
if (appSelector) {
|
||||
setAppSelector(prevState => {
|
||||
if (prevState === appSelector) {
|
||||
setLoadingData(false);
|
||||
return appSelector;
|
||||
}
|
||||
setLoadingData(true);
|
||||
|
||||
return appSelector;
|
||||
});
|
||||
} else {
|
||||
setAppName(oldName => {
|
||||
if (oldName === appName) {
|
||||
setLoadingData(false);
|
||||
return appName;
|
||||
}
|
||||
setLoadingData(true);
|
||||
|
||||
return appName;
|
||||
});
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [loading, appSelector, appName]);
|
||||
|
||||
return { apps, error, loading: loadingData };
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { configApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
import { Instances } from '../types';
|
||||
|
||||
export const useArgocdConfig = (): {
|
||||
instances: Instances;
|
||||
intervalMs: number;
|
||||
baseUrl: string | undefined;
|
||||
} => {
|
||||
const configApi = useApi(configApiRef);
|
||||
|
||||
const instances = (
|
||||
configApi
|
||||
.getConfigArray('argocd.appLocatorMethods')
|
||||
.find(value => value.getOptionalString('type') === 'config')
|
||||
?.getOptionalConfigArray('instances') ?? []
|
||||
).map(config => ({
|
||||
name: config.getOptionalString('name') ?? '',
|
||||
url: config.getOptionalString('url') ?? '',
|
||||
}));
|
||||
const intervalMs =
|
||||
configApi.getOptionalNumber('argocd.refreshInterval') ?? 10000;
|
||||
const baseUrl = configApi.getOptionalString('argocd.baseUrl');
|
||||
return {
|
||||
baseUrl,
|
||||
instances,
|
||||
intervalMs,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { usePermission } from '@backstage/plugin-permission-react';
|
||||
|
||||
import { argocdViewPermission } from '@janus-idp/backstage-plugin-argocd-common';
|
||||
|
||||
export const useArgocdViewPermission = () => {
|
||||
const argocdViewPermissionResult = usePermission({
|
||||
permission: argocdViewPermission,
|
||||
});
|
||||
|
||||
return argocdViewPermissionResult.allowed;
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export {
|
||||
argocdPlugin,
|
||||
ArgocdDeploymentLifecycle,
|
||||
ArgocdDeploymentSummary,
|
||||
} from './plugin';
|
||||
export { isArgocdConfigured } from './utils/isArgocdConfigured';
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import React from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { configApiRef } from '@backstage/core-plugin-api';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
import { permissionApiRef } from '@backstage/plugin-permission-react';
|
||||
import {
|
||||
MockConfigApi,
|
||||
MockPermissionApi,
|
||||
renderInTestApp,
|
||||
TestApiProvider,
|
||||
} from '@backstage/test-utils';
|
||||
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import { mockApplication, mockEntity, mockRevision } from '../dev/__data__';
|
||||
import { argoCDApiRef } from './api';
|
||||
import {
|
||||
ArgocdDeploymentLifecycle,
|
||||
ArgocdDeploymentSummary,
|
||||
argocdPlugin,
|
||||
} from './plugin';
|
||||
import { rootRouteRef } from './routes';
|
||||
|
||||
describe('argocd', () => {
|
||||
const mockedApi: any = {
|
||||
listApps: async () => {
|
||||
return { items: [mockApplication] };
|
||||
},
|
||||
getRevisionDetails: async () => {
|
||||
return mockRevision;
|
||||
},
|
||||
getRevisionDetailsList: async () => {
|
||||
return [mockRevision];
|
||||
},
|
||||
};
|
||||
|
||||
const mockPermissionApi = new MockPermissionApi();
|
||||
|
||||
const mockConfiguration = new MockConfigApi({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:7007',
|
||||
listen: {
|
||||
port: 7007,
|
||||
},
|
||||
},
|
||||
argocd: {
|
||||
appLocatorMethods: [],
|
||||
},
|
||||
});
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[argoCDApiRef, mockedApi],
|
||||
[configApiRef, mockConfiguration],
|
||||
[permissionApiRef, mockPermissionApi],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={mockEntity}>{children}</EntityProvider>
|
||||
</TestApiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
it('should export plugin', () => {
|
||||
// const app = createDevApp().registerPlugin(argocdPlugin).build();
|
||||
|
||||
const [argoApi] = argocdPlugin.getApis();
|
||||
expect(argocdPlugin).toBeDefined();
|
||||
|
||||
expect(
|
||||
argoApi.factory({
|
||||
identityApi: { getCredentials: () => {} },
|
||||
configApi: mockConfiguration,
|
||||
}),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render the deployment lifecycle extension', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<Routes>
|
||||
<Route path="/" element={<ArgocdDeploymentLifecycle />} />
|
||||
</Routes>
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/': rootRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Deployment lifecycle')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the deployment summary extension', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<Routes>
|
||||
<Route path="/" element={<ArgocdDeploymentSummary />} />
|
||||
</Routes>
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/': rootRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Deployment summary')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import '@patternfly/react-core/dist/styles/base.css';
|
||||
import '@patternfly/patternfly/patternfly-theme-dark.css';
|
||||
|
||||
import {
|
||||
configApiRef,
|
||||
createApiFactory,
|
||||
createPlugin,
|
||||
createRoutableExtension,
|
||||
identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
import { ArgoCDApiClient, argoCDApiRef } from './api';
|
||||
import { rootRouteRef } from './routes';
|
||||
|
||||
export const argocdPlugin = createPlugin({
|
||||
id: 'rh-argocd',
|
||||
routes: {
|
||||
root: rootRouteRef,
|
||||
},
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: argoCDApiRef,
|
||||
deps: {
|
||||
identityApi: identityApiRef,
|
||||
configApi: configApiRef,
|
||||
},
|
||||
factory: ({ identityApi, configApi }) =>
|
||||
new ArgoCDApiClient({
|
||||
identityApi,
|
||||
backendBaseUrl: configApi.getString('backend.baseUrl'),
|
||||
useNamespacedApps: Boolean(
|
||||
configApi.getOptionalBoolean('argocd.namespacedApps'),
|
||||
),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export const ArgocdDeploymentLifecycle = argocdPlugin.provide(
|
||||
createRoutableExtension({
|
||||
name: 'ArgocdDeploymentLifecycle',
|
||||
component: () =>
|
||||
import('./components/DeploymentLifeCycle').then(
|
||||
m => m.DeploymentLifecycle,
|
||||
),
|
||||
mountPoint: rootRouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
export const ArgocdDeploymentSummary = argocdPlugin.provide(
|
||||
createRoutableExtension({
|
||||
name: 'ArgocdDeploymentSummary',
|
||||
component: () =>
|
||||
import('./components/DeploymentSummary').then(m => m.DeploymentSummary),
|
||||
mountPoint: rootRouteRef,
|
||||
}),
|
||||
);
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { createRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
id: 'rh-argocd',
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
import '@testing-library/jest-dom';
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
import { V1ObjectMeta } from '@kubernetes/client-node';
|
||||
|
||||
export interface Application {
|
||||
apiVersion?: string;
|
||||
kind?: string;
|
||||
metadata: V1ObjectMeta & { instance: Instance };
|
||||
spec: Spec;
|
||||
status: Status;
|
||||
operation?: Operation;
|
||||
isAppOfAppsPattern?: boolean;
|
||||
}
|
||||
|
||||
export interface Instance {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Spec {
|
||||
source: Source;
|
||||
destination: Destination;
|
||||
project: string;
|
||||
}
|
||||
|
||||
export interface Destination {
|
||||
server: string;
|
||||
namespace: string;
|
||||
}
|
||||
|
||||
export interface Source {
|
||||
chart?: string;
|
||||
repoURL: string;
|
||||
path: string;
|
||||
helm?: {
|
||||
parameters: {
|
||||
name: string;
|
||||
value: string;
|
||||
}[];
|
||||
};
|
||||
targetRevision?: string;
|
||||
}
|
||||
|
||||
export interface Status {
|
||||
sync: StatusSync;
|
||||
health: Health;
|
||||
operationState: OperationState;
|
||||
resources?: StatusResource[];
|
||||
history?: History[];
|
||||
reconciledAt?: string;
|
||||
sourceType?: string;
|
||||
summary: Summary;
|
||||
}
|
||||
|
||||
export interface Health {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface History {
|
||||
revision: string;
|
||||
deployedAt: string;
|
||||
id: number;
|
||||
source: Source;
|
||||
deployStartedAt: string;
|
||||
}
|
||||
|
||||
export interface OperationState {
|
||||
operation: Operation;
|
||||
phase?: string;
|
||||
message?: string;
|
||||
syncResult?: SyncResult;
|
||||
startedAt?: string;
|
||||
finishedAt?: string;
|
||||
}
|
||||
|
||||
export interface Operation {
|
||||
sync: OperationSync;
|
||||
initiatedBy?: InitiatedBy;
|
||||
retry?: {};
|
||||
}
|
||||
|
||||
export interface InitiatedBy {
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface OperationSync {
|
||||
prune?: boolean;
|
||||
revision: string;
|
||||
syncStrategy?: SyncStrategy;
|
||||
syncOptions?: string[];
|
||||
}
|
||||
|
||||
export interface SyncStrategy {
|
||||
hook: {};
|
||||
}
|
||||
|
||||
export interface SyncResult {
|
||||
resources: SyncResultResource[];
|
||||
revision: string;
|
||||
source: Source;
|
||||
}
|
||||
|
||||
export interface SyncResultResource {
|
||||
group: string;
|
||||
version: string;
|
||||
kind: string;
|
||||
namespace: string;
|
||||
name: string;
|
||||
status: string;
|
||||
message: string;
|
||||
hookPhase: string;
|
||||
syncPhase: string;
|
||||
}
|
||||
|
||||
export interface StatusResource {
|
||||
version: string;
|
||||
kind: string;
|
||||
namespace: string;
|
||||
name: string;
|
||||
status: string;
|
||||
health: Health;
|
||||
group?: string;
|
||||
}
|
||||
|
||||
export interface Summary {
|
||||
images: string[];
|
||||
}
|
||||
|
||||
export interface StatusSync {
|
||||
status: string;
|
||||
comparedTo?: {
|
||||
source: Source;
|
||||
destination: Destination;
|
||||
};
|
||||
revision?: string;
|
||||
}
|
||||
|
||||
export interface Revision {
|
||||
author: string;
|
||||
date: Date;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export enum HealthStatus {
|
||||
Healthy = 'Healthy',
|
||||
Suspended = 'Suspended',
|
||||
Degraded = 'Degraded',
|
||||
Progressing = 'Progressing',
|
||||
Missing = 'Missing',
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
export type SyncStatusCode = 'Unknown' | 'Synced' | 'OutOfSync';
|
||||
|
||||
export const SyncStatuses: { [key: string]: SyncStatusCode } = {
|
||||
Synced: 'Synced',
|
||||
Unknown: 'Unknown',
|
||||
OutOfSync: 'OutOfSync',
|
||||
};
|
||||
|
||||
export type OperationPhase =
|
||||
| 'Running'
|
||||
| 'Error'
|
||||
| 'Failed'
|
||||
| 'Succeeded'
|
||||
| 'Terminating';
|
||||
|
||||
export const OperationPhases: { [key: string]: OperationPhase } = {
|
||||
Running: 'Running',
|
||||
Failed: 'Failed',
|
||||
Error: 'Error',
|
||||
Succeeded: 'Succeeded',
|
||||
Terminating: 'Terminating',
|
||||
};
|
||||
|
||||
export type Instances = {
|
||||
name: string;
|
||||
url: string;
|
||||
}[];
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { mockEntity } from '../../../dev/__data__';
|
||||
import { isArgocdConfigured } from '../isArgocdConfigured';
|
||||
|
||||
describe('isArgocdConfigured', () => {
|
||||
test('should return false if argocd is not configured', () => {
|
||||
expect(
|
||||
isArgocdConfigured({
|
||||
...mockEntity,
|
||||
metadata: {
|
||||
...mockEntity.metadata,
|
||||
annotations: {},
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true if argocd is configured', () => {
|
||||
expect(isArgocdConfigured(mockEntity)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
import { mockApplication, mockEntity } from '../../../dev/__data__';
|
||||
import { Application, History, Status } from '../../types';
|
||||
import {
|
||||
ArgoCdLabels,
|
||||
getAppOperationState,
|
||||
getAppSelector,
|
||||
getArgoCdAppConfig,
|
||||
getCommitUrl,
|
||||
getGitProvider,
|
||||
getInstanceName,
|
||||
getProjectName,
|
||||
getUniqueRevisions,
|
||||
} from '../utils';
|
||||
|
||||
describe('Utils', () => {
|
||||
describe('getAppSelector', () => {
|
||||
test('should return an empty string if the app selector is not set', () => {
|
||||
expect(
|
||||
getAppSelector({
|
||||
...mockEntity,
|
||||
metadata: {
|
||||
...mockEntity.metadata,
|
||||
annotations: {},
|
||||
},
|
||||
}),
|
||||
).toBe('');
|
||||
});
|
||||
|
||||
test('should return the app selector string', () => {
|
||||
expect(getAppSelector(mockEntity)).toBe(
|
||||
'rht-gitops.com/janus-argocd=quarkus-app-bootstrap',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInstanceName', () => {
|
||||
test('should return an default instance if the instance name is not set', () => {
|
||||
expect(
|
||||
getInstanceName({
|
||||
...mockEntity,
|
||||
metadata: {
|
||||
...mockEntity.metadata,
|
||||
annotations: {},
|
||||
},
|
||||
}),
|
||||
).toBe('');
|
||||
});
|
||||
|
||||
test('should return the instance name', () => {
|
||||
expect(getInstanceName(mockEntity)).toBe('instance-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProjectName', () => {
|
||||
test('should return undefined for project name', () => {
|
||||
expect(
|
||||
getProjectName({
|
||||
...mockEntity,
|
||||
metadata: {
|
||||
...mockEntity.metadata,
|
||||
annotations: {},
|
||||
},
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should return the project name', () => {
|
||||
expect(getProjectName(mockEntity)).toBe('project-name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGitProvider', () => {
|
||||
test('should return unknown provider if the annotations are empty', () => {
|
||||
expect(getGitProvider({})).toBe('unknown');
|
||||
expect(getGitProvider(null as unknown as { [key: string]: string })).toBe(
|
||||
'unknown',
|
||||
);
|
||||
});
|
||||
|
||||
test('should return gitlab if the url contain gitlab.com', () => {
|
||||
expect(
|
||||
getGitProvider({
|
||||
'test.com/source-url': 'https://gitlab.com/testrepo',
|
||||
}),
|
||||
).toBe('gitlab');
|
||||
});
|
||||
|
||||
test('should return github if the url contain github.com', () => {
|
||||
expect(
|
||||
getGitProvider({
|
||||
'test.com/source-url': 'https://github.com/testrepo',
|
||||
}),
|
||||
).toBe('github');
|
||||
});
|
||||
|
||||
test('should return gitlab based on the annotation key', () => {
|
||||
expect(
|
||||
getGitProvider({
|
||||
'gitlab.com/source-url': 'https://custom-gl-url.com/testrepo',
|
||||
}),
|
||||
).toBe('gitlab');
|
||||
});
|
||||
|
||||
test('should return github based on the annotation key', () => {
|
||||
expect(
|
||||
getGitProvider({
|
||||
'github.com/source-url': 'https://custom-gh-url.com/testrepo',
|
||||
'test-key': 'test-value',
|
||||
}),
|
||||
).toBe('github');
|
||||
});
|
||||
|
||||
test('should return unknown if annotation key and url does not contain known providers', () => {
|
||||
expect(
|
||||
getGitProvider({
|
||||
'bitbucket.com/source-url':
|
||||
'https://custom-bitbucket-url.com/testrepo',
|
||||
'test-key': 'test-value',
|
||||
}),
|
||||
).toBe('unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCommitUrl', () => {
|
||||
test('should return correct gitlab commit url', () => {
|
||||
expect(
|
||||
getCommitUrl('https://custom-gl-url.com/testrepo', '12345', {
|
||||
'gitlab.com/source-url': 'https://custom-gl-url.com/testrepo',
|
||||
}),
|
||||
).toBe('https://custom-gl-url.com/testrepo/-/commit/12345');
|
||||
});
|
||||
|
||||
test('should return correct github commit url', () => {
|
||||
expect(
|
||||
getCommitUrl('https://custom-gh-url.com/testrepo', '12345', {
|
||||
'github.com/source-url': 'https://custom-gl-url.com/testrepo',
|
||||
}),
|
||||
).toBe('https://custom-gh-url.com/testrepo/commit/12345');
|
||||
});
|
||||
|
||||
test('should return the repo url for unknown provider', () => {
|
||||
expect(
|
||||
getCommitUrl('https://custom-gh-url.com/testrepo', '12345', {
|
||||
'bitbucket.com/source-url': 'https://custom-bb-url.com/testrepo',
|
||||
}),
|
||||
).toBe('https://custom-gh-url.com/testrepo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAppOperationState', () => {
|
||||
test('should return Succeeded if the operationState object is present', () => {
|
||||
expect(getAppOperationState(mockApplication).phase).toBe('Succeeded');
|
||||
});
|
||||
|
||||
test('should return Running if the application has operation object outside', () => {
|
||||
expect(
|
||||
getAppOperationState({
|
||||
...mockApplication,
|
||||
operation: {
|
||||
...mockApplication.status.operationState.operation,
|
||||
},
|
||||
}).phase,
|
||||
).toBe('Running');
|
||||
});
|
||||
test('should return Running if the application is marked for deletion', () => {
|
||||
expect(
|
||||
getAppOperationState({
|
||||
...mockApplication,
|
||||
metadata: {
|
||||
...mockApplication.metadata,
|
||||
deletionTimestamp: new Date(),
|
||||
},
|
||||
}).phase,
|
||||
).toBe('Running');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUniqueRevisions', () => {
|
||||
test('should return empty array for invalid values', () => {
|
||||
expect(getUniqueRevisions([])).toHaveLength(0);
|
||||
|
||||
expect(getUniqueRevisions(null as unknown as Application[])).toHaveLength(
|
||||
0,
|
||||
);
|
||||
|
||||
expect(
|
||||
getUniqueRevisions([
|
||||
{
|
||||
...mockApplication,
|
||||
status: undefined as unknown as Status,
|
||||
},
|
||||
]),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should return unique revision', () => {
|
||||
expect(getUniqueRevisions([mockApplication])).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should return unique revision for multiple applications', () => {
|
||||
const mockApplicationTwo: Application = {
|
||||
...mockApplication,
|
||||
status: {
|
||||
...mockApplication.status,
|
||||
history: [
|
||||
{
|
||||
...(mockApplication?.status?.history?.[0] as History),
|
||||
revision: '12345',
|
||||
},
|
||||
{
|
||||
...(mockApplication?.status?.history?.[1] as History),
|
||||
revision: '12345',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(getUniqueRevisions([mockApplication, mockApplicationTwo])).toEqual(
|
||||
['90f9758b7033a4bbb7c33a35ee474d61091644bc', '12345'],
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('getArgoCdAppConfig', () => {
|
||||
test('should throw error if both the annotations are missing', () => {
|
||||
expect(() =>
|
||||
getArgoCdAppConfig({
|
||||
entity: {
|
||||
...mockEntity,
|
||||
metadata: {
|
||||
...mockEntity.metadata,
|
||||
annotations: {},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toThrow('Argo CD annotation is missing in the catalog');
|
||||
});
|
||||
|
||||
test('should throw error if both the annotations are set', () => {
|
||||
expect(() =>
|
||||
getArgoCdAppConfig({
|
||||
entity: {
|
||||
...mockEntity,
|
||||
metadata: {
|
||||
...mockEntity.metadata,
|
||||
annotations: {
|
||||
[ArgoCdLabels.appSelector]: 'janus-idp/quarkus-app',
|
||||
[ArgoCdLabels.appName]: 'quarkus-app',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toThrow(
|
||||
`Cannot provide both ${ArgoCdLabels.appName} and ${ArgoCdLabels.appSelector} annotations`,
|
||||
);
|
||||
});
|
||||
|
||||
test('should return argocd configuration object when one of the annotations is set', () => {
|
||||
const configuration = getArgoCdAppConfig({ entity: mockEntity });
|
||||
|
||||
expect(configuration).toStrictEqual({
|
||||
appName: '',
|
||||
appNamespace: '',
|
||||
appSelector: 'rht-gitops.com%2Fjanus-argocd%3Dquarkus-app-bootstrap',
|
||||
projectName: 'project-name',
|
||||
url: '/argocd/api',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
import { ArgoCdLabels } from './utils';
|
||||
|
||||
export const isArgocdConfigured = (entity: Entity) =>
|
||||
Boolean(entity?.metadata.annotations?.[ArgoCdLabels.appSelector]) ||
|
||||
Boolean(entity?.metadata.annotations?.[ArgoCdLabels.appName]);
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
import { Application, OperationPhases, OperationState } from '../types';
|
||||
|
||||
export const enum ArgoCdLabels {
|
||||
appSelector = 'argocd/app-selector',
|
||||
instanceName = 'argocd/instance-name',
|
||||
projectName = 'argocd/project-name',
|
||||
appName = 'argocd/app-name',
|
||||
appNamespace = 'argocd/app-namespace',
|
||||
}
|
||||
|
||||
export const getAppSelector = (entity: Entity): string => {
|
||||
return entity?.metadata?.annotations?.[ArgoCdLabels.appSelector] ?? '';
|
||||
};
|
||||
export const getAppName = (entity: Entity): string => {
|
||||
return entity?.metadata?.annotations?.[ArgoCdLabels.appName] ?? '';
|
||||
};
|
||||
export const getAppNamespace = (entity: Entity): string => {
|
||||
return entity?.metadata?.annotations?.[ArgoCdLabels.appNamespace] ?? '';
|
||||
};
|
||||
|
||||
export const getInstanceName = (entity: Entity): string => {
|
||||
return entity?.metadata?.annotations?.[ArgoCdLabels.instanceName] ?? '';
|
||||
};
|
||||
|
||||
export const getProjectName = (entity: Entity): string | undefined => {
|
||||
return entity?.metadata?.annotations?.[ArgoCdLabels.projectName];
|
||||
};
|
||||
|
||||
export const getArgoCdAppConfig = ({ entity }: { entity: Entity }) => {
|
||||
const appName = getAppName(entity);
|
||||
const appSelector = encodeURIComponent(getAppSelector(entity));
|
||||
const appNamespace = getAppNamespace(entity);
|
||||
const projectName = getProjectName(entity);
|
||||
const url = '/argocd/api';
|
||||
|
||||
if (!(appName || appSelector)) {
|
||||
throw new Error('Argo CD annotation is missing in the catalog');
|
||||
} else if (appName && appSelector) {
|
||||
throw new Error(
|
||||
`Cannot provide both ${ArgoCdLabels.appName} and ${ArgoCdLabels.appSelector} annotations`,
|
||||
);
|
||||
}
|
||||
return { url, appName, appSelector, appNamespace, projectName };
|
||||
};
|
||||
|
||||
type ProviderType = 'github' | 'gitlab' | 'unknown';
|
||||
|
||||
export enum Providers {
|
||||
github = 'github',
|
||||
gitlab = 'gitlab',
|
||||
unknown = 'unknown',
|
||||
}
|
||||
|
||||
export enum KnownProviders {
|
||||
gitlab = 'gitlab.com',
|
||||
github = 'github.com',
|
||||
}
|
||||
|
||||
export const isAppHelmChartType = (application: Application) =>
|
||||
!!application?.spec?.source?.chart;
|
||||
|
||||
export const getGitProvider = (annotations: {
|
||||
[key: string]: string;
|
||||
}): ProviderType => {
|
||||
const entityKeys = Object.keys(annotations ?? {});
|
||||
if (entityKeys.length === 0) {
|
||||
return Providers.unknown;
|
||||
}
|
||||
let provider: ProviderType = Providers.unknown;
|
||||
entityKeys.forEach(key => {
|
||||
if (provider !== Providers.unknown) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
key.startsWith(KnownProviders.gitlab) ||
|
||||
annotations[key].includes(KnownProviders.gitlab)
|
||||
) {
|
||||
provider = Providers.gitlab;
|
||||
} else if (
|
||||
key.startsWith(KnownProviders.github) ||
|
||||
annotations[key].includes(KnownProviders.github)
|
||||
) {
|
||||
provider = Providers.github;
|
||||
} else {
|
||||
provider = Providers.unknown;
|
||||
}
|
||||
});
|
||||
|
||||
return provider;
|
||||
};
|
||||
|
||||
export const getCommitUrl = (
|
||||
url: string,
|
||||
revisionId: string,
|
||||
annotations: { [key: string]: string },
|
||||
) => {
|
||||
const sanitizedUrl = url.replace('.git', '');
|
||||
const providerCommitPrefix: { [key in ProviderType]: string } = {
|
||||
gitlab: '/-/commit/',
|
||||
github: '/commit/',
|
||||
unknown: '',
|
||||
};
|
||||
|
||||
let provider: ProviderType = Providers.unknown;
|
||||
|
||||
if (provider === Providers.unknown) {
|
||||
provider = getGitProvider(annotations);
|
||||
}
|
||||
|
||||
return provider === Providers.unknown
|
||||
? sanitizedUrl
|
||||
: `${sanitizedUrl}${providerCommitPrefix[provider]}${revisionId}`;
|
||||
};
|
||||
|
||||
export const getAppOperationState = (app: Application): OperationState => {
|
||||
if (app.operation) {
|
||||
return {
|
||||
phase: OperationPhases.Running,
|
||||
message: app?.status?.operationState?.message || 'waiting to start',
|
||||
startedAt: new Date().toISOString(),
|
||||
operation: {
|
||||
sync: {},
|
||||
},
|
||||
} as OperationState;
|
||||
} else if (app.metadata.deletionTimestamp) {
|
||||
return {
|
||||
phase: OperationPhases.Running,
|
||||
message: '',
|
||||
startedAt: app.metadata.deletionTimestamp,
|
||||
operation: {
|
||||
sync: {},
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
return app.status.operationState;
|
||||
};
|
||||
|
||||
export const getUniqueRevisions = (apps: Application[]): string[] =>
|
||||
apps
|
||||
? apps.reduce((acc, app) => {
|
||||
const history = app?.status?.history ?? [];
|
||||
const revisions: string[] = [];
|
||||
|
||||
if (history.length > 0) {
|
||||
history.forEach(h => {
|
||||
if (
|
||||
!revisions.includes(h.revision as string) &&
|
||||
!isAppHelmChartType(app)
|
||||
) {
|
||||
revisions.push(h.revision);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
revisions.forEach((rev: string) => {
|
||||
if (!acc.includes(rev)) {
|
||||
acc.push(rev);
|
||||
}
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, [] as string[])
|
||||
: [];
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
import { expect, Page, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
mockApplication,
|
||||
preProdApplication,
|
||||
prodApplication,
|
||||
} from '../dev/__data__';
|
||||
import { Common } from './argocdHelper';
|
||||
import { verifyAppCard, verifyAppSidebar } from './utils';
|
||||
|
||||
test.describe('ArgoCD plugin', () => {
|
||||
let argocdPage: Page;
|
||||
let common: Common;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
const context = await browser.newContext();
|
||||
argocdPage = await context.newPage();
|
||||
common = new Common(argocdPage);
|
||||
|
||||
await common.loginAsGuest();
|
||||
await expect(
|
||||
argocdPage.getByRole('heading', { name: 'Deployment lifecycle' }),
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
});
|
||||
|
||||
test.afterAll(async ({ browser }) => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
const apps = [mockApplication, preProdApplication, prodApplication];
|
||||
|
||||
test.describe('Deployment lifecycle', () => {
|
||||
for (const [index, app] of apps.entries()) {
|
||||
/* eslint-disable-next-line no-loop-func */
|
||||
test(`Verify ${app.metadata.name} card`, async () => {
|
||||
const card = argocdPage.getByTestId(`${app.metadata.name}-card`);
|
||||
await expect(card).toBeVisible();
|
||||
await verifyAppCard(app, card, index);
|
||||
});
|
||||
|
||||
/* eslint-disable-next-line no-loop-func */
|
||||
test(`Verify ${app.metadata.name} side bar`, async () => {
|
||||
await argocdPage.getByTestId(`${app.metadata.name}-card`).click();
|
||||
const sideBar = argocdPage.locator(`.MuiDrawer-paper`);
|
||||
await expect(sideBar).toBeVisible();
|
||||
await verifyAppSidebar(app, sideBar, index);
|
||||
await sideBar.getByRole('button', { name: 'Close the drawer' }).click();
|
||||
await expect(sideBar).toBeVisible({ visible: false });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('Summary', () => {
|
||||
const columns = [
|
||||
'ArgoCD App',
|
||||
'Namespace',
|
||||
'Instance',
|
||||
'Server',
|
||||
'Revision',
|
||||
'Last Deployed',
|
||||
'Sync Status',
|
||||
'Health Status',
|
||||
];
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await argocdPage.getByRole('link', { name: 'Summary' }).click();
|
||||
await expect(argocdPage.getByRole('heading')).toHaveText(
|
||||
'Deployment summary',
|
||||
);
|
||||
});
|
||||
|
||||
test('Verify column names', async () => {
|
||||
for (const col of columns) {
|
||||
await expect(
|
||||
argocdPage.getByRole('columnheader', { name: col }),
|
||||
).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
for (const app of apps) {
|
||||
const appName = app.metadata.name;
|
||||
|
||||
/* eslint-disable-next-line no-loop-func */
|
||||
test(`Verify ${appName} row`, async () => {
|
||||
const row = argocdPage.locator('.MuiTableRow-root', {
|
||||
hasText: appName,
|
||||
});
|
||||
const revision = app.status.history
|
||||
?.slice(-1)[0]
|
||||
.revision.substring(0, 7);
|
||||
|
||||
await expect(
|
||||
row.locator('td', { hasText: app.metadata.instance.name }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
row.locator('td', { hasText: app.spec.destination.server }),
|
||||
).toBeVisible();
|
||||
await expect(row.locator('td', { hasText: revision })).toBeVisible();
|
||||
await expect(
|
||||
row.locator('td', { hasText: app.status.health.status }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
row.locator('td', { hasText: app.status.sync.status }),
|
||||
).toBeVisible();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { expect, type Locator, type Page } from '@playwright/test';
|
||||
|
||||
export class Common {
|
||||
page: Page;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async verifyHeading(heading: string) {
|
||||
const headingLocator = this.page
|
||||
.locator('h1, h2, h3, h4, h5, h6')
|
||||
.filter({ hasText: heading })
|
||||
.first();
|
||||
await headingLocator.waitFor({ state: 'visible', timeout: 30000 });
|
||||
await expect(headingLocator).toBeVisible();
|
||||
}
|
||||
|
||||
async clickButton(
|
||||
label: string,
|
||||
clickOpts?: Parameters<Locator['click']>[0],
|
||||
getByTextOpts: Parameters<Locator['getByText']>[1] = { exact: true },
|
||||
) {
|
||||
const muiButtonLabel = 'span[class^="MuiButton-label"]';
|
||||
const selector = `${muiButtonLabel}:has-text("${label}")`;
|
||||
const button = this.page
|
||||
.locator(selector)
|
||||
.getByText(label, getByTextOpts)
|
||||
.first();
|
||||
await button.waitFor({ state: 'visible' });
|
||||
await button.click(clickOpts);
|
||||
}
|
||||
|
||||
async waitForSideBarVisible() {
|
||||
await this.page.waitForSelector('nav a', { timeout: 120000 });
|
||||
}
|
||||
|
||||
async loginAsGuest() {
|
||||
await this.page.goto('/');
|
||||
// TODO - Remove it after https://issues.redhat.com/browse/RHIDP-2043. A Dynamic plugin for Guest Authentication Provider needs to be created
|
||||
this.page.on('dialog', async dialog => {
|
||||
await dialog.accept();
|
||||
});
|
||||
|
||||
await this.verifyHeading('Select a sign-in method');
|
||||
await this.clickButton('Enter');
|
||||
await this.waitForSideBarVisible();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
import { expect, Locator } from '@playwright/test';
|
||||
|
||||
import { mockArgocdConfig, mockRevisions } from '../dev/__data__';
|
||||
import { Application, History } from '../src/types';
|
||||
|
||||
export const verifyHeader = async (app: Application, card: Locator) => {
|
||||
const header = card.locator('.MuiCardHeader-content');
|
||||
await expect(header.getByText(`${app.metadata.name}`)).toBeVisible();
|
||||
|
||||
const appUrl = `${mockArgocdConfig.argocd.baseUrl}/applications/${app.metadata.name}`;
|
||||
await expect(header.getByRole('link')).toHaveAttribute('href', appUrl);
|
||||
await expect(header.getByTestId('app-sync-status-chip')).toHaveText(
|
||||
app.status.sync.status,
|
||||
);
|
||||
await expect(header.getByTestId('app-health-status-chip')).toHaveText(
|
||||
app.status.health.status,
|
||||
);
|
||||
};
|
||||
|
||||
export const verifyItem = async (
|
||||
name: string,
|
||||
content: string | string[],
|
||||
card: Locator,
|
||||
unique = true,
|
||||
) => {
|
||||
const item = card.locator('.MuiGrid-item', { hasText: name });
|
||||
const result = unique ? item : item.first();
|
||||
await expect(result).toContainText(content);
|
||||
};
|
||||
|
||||
export const verifyDeployments = async (
|
||||
app: Application,
|
||||
sideBar: Locator,
|
||||
index: number,
|
||||
) => {
|
||||
const imageUrl = `https://${app.status.summary.images[0]}`;
|
||||
const image = imageUrl.split('/').pop();
|
||||
const latestDeploy = app.status.history?.slice(-1)[0];
|
||||
const deployHistory = app.status.history?.slice(0, -1) as History[];
|
||||
const shortRevision = `${latestDeploy?.revision.substring(0, 7)}`;
|
||||
|
||||
const latest = sideBar.locator('.MuiGrid-item', {
|
||||
hasText: 'Latest deployment',
|
||||
});
|
||||
await expect(latest).toContainText(
|
||||
`Image ${image}${mockRevisions[index].message} ${shortRevision}`,
|
||||
);
|
||||
await expect(latest.getByRole('link', { name: image })).toHaveAttribute(
|
||||
'href',
|
||||
imageUrl,
|
||||
);
|
||||
const revisionUrl = latestDeploy?.source.repoURL.substring(
|
||||
0,
|
||||
latestDeploy?.source.repoURL.lastIndexOf('.'),
|
||||
);
|
||||
await expect(
|
||||
latest.getByRole('link', { name: shortRevision }),
|
||||
).toHaveAttribute('href', `${revisionUrl}`);
|
||||
|
||||
const history = sideBar.locator('.MuiGrid-item', {
|
||||
hasText: 'Deployment history',
|
||||
});
|
||||
const items = history.locator('.MuiCard-root', { hasText: 'Deployment' });
|
||||
await expect(items).toHaveCount(deployHistory.length);
|
||||
|
||||
for (const item of await items.all()) {
|
||||
await expect(item).toContainText(
|
||||
`${mockRevisions[index].message} ${shortRevision}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyAppCard = async (
|
||||
app: Application,
|
||||
card: Locator,
|
||||
index: number,
|
||||
) => {
|
||||
await verifyHeader(app, card);
|
||||
await verifyItem('Instance', 'main', card);
|
||||
await verifyItem(
|
||||
'Server',
|
||||
`${app.spec.destination.server} (in-cluster)`,
|
||||
card,
|
||||
);
|
||||
await verifyItem('Namespace', app.spec.destination.namespace, card);
|
||||
|
||||
const revision = app.status.history
|
||||
?.slice(-1)[0]
|
||||
.revision.substring(0, 7) as string;
|
||||
await verifyItem(
|
||||
'Commit',
|
||||
`${revision}${mockRevisions[index].message}`,
|
||||
card,
|
||||
);
|
||||
|
||||
const image = app.status.summary.images[0].split('/').pop();
|
||||
await verifyItem('Deployment', `Image ${image}`, card);
|
||||
};
|
||||
|
||||
export const verifyAppSidebar = async (
|
||||
app: Application,
|
||||
sideBar: Locator,
|
||||
index: number,
|
||||
) => {
|
||||
await verifyItem(
|
||||
`${app.metadata.name}`,
|
||||
`${app.status.sync.status}${app.status.health.status}`,
|
||||
sideBar,
|
||||
false,
|
||||
);
|
||||
await verifyItem('Instance', 'main', sideBar);
|
||||
await verifyItem(
|
||||
'Server',
|
||||
`${app.spec.destination.server} (in-cluster)`,
|
||||
sideBar,
|
||||
);
|
||||
await verifyItem('Namespace', app.spec.destination.namespace, sideBar);
|
||||
|
||||
const revision = app.status.history
|
||||
?.slice(-1)[0]
|
||||
.revision.substring(0, 7) as string;
|
||||
await verifyItem(
|
||||
'Commit',
|
||||
`${revision}${mockRevisions[index].message} by ${mockRevisions[index].author}`,
|
||||
sideBar,
|
||||
false,
|
||||
);
|
||||
|
||||
await verifyDeployments(app, sideBar, index);
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "@backstage/cli/config/tsconfig.json",
|
||||
"include": ["src", "dev"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist-types/plugins/argocd",
|
||||
"rootDir": "."
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": ["//"],
|
||||
"pipeline": {
|
||||
"tsc": {
|
||||
"outputs": ["../../dist-types/plugins/argocd/**"],
|
||||
"dependsOn": ["^tsc"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"extends": "@backstage/cli/config/tsconfig.json",
|
||||
"include": [
|
||||
"packages/*/src",
|
||||
"plugins/*/src",
|
||||
"plugins/*/dev",
|
||||
"plugins/*/migrations"
|
||||
],
|
||||
"files": ["node_modules/@backstage/cli/asset-types/asset-types.d.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist-types",
|
||||
"rootDir": ".",
|
||||
"lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2022"],
|
||||
"target": "ES2022",
|
||||
"useUnknownInCatchVariables": false
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue