Compare commits

...

15 Commits

Author SHA1 Message Date
Jonas Beck 3390a1d16d
chore: policy reports empty state component (#66) 2025-06-29 07:32:55 +02:00
Jonas Beck 0410f338dd
chore(tsconfig): add packages/ to tsconfig include to avoid warnings (#65) 2025-06-28 09:38:20 +02:00
github-actions[bot] 39e9a8bdb1
Version Packages (#64)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-27 14:21:37 +02:00
Jonas Beck 0d6cf4bc8d
feat: add PolicyReportsPage status and severity filter (#63)
Updates the PolicyReportsPage component to now render 1 big table showing all failed policies. It's now also possible to filter policies by status and severity

---------

Signed-off-by: Jonas Beck <dev@jonasbeck.dk>
2025-06-24 07:43:59 +02:00
Andrew Thauer 5560993004
chore: add policy report page to example app (#62)
Signed-off-by: Andrew Thauer <athauer@wealthsimple.com>
2025-06-17 07:28:57 +02:00
github-actions[bot] 68cf94e428
Version Packages (#61)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-16 11:25:05 +02:00
Jonas Beck 705c577db3
chore(changeset): ignore app and backend (#60)
Signed-off-by: Jonas Beck <dev@jonasbeck.dk>
2025-06-16 11:15:45 +02:00
Jonas Beck 274db4db8c
feat: policy reporter page (#58)
Add an initial implementation of a PolicyReportsPage component that should give a more global overview of all policy reports from a given cluster.

---------

Signed-off-by: Jonas Beck <dev@jonasbeck.dk>
2025-06-16 10:57:16 +02:00
Andrew Thauer 6b20c9cfb4
chore: setup backstage repo-tools workspace dev tooling (#49)
Signed-off-by: Andrew Thauer <athauer@wealthsimple.com>
2025-06-16 10:55:30 +02:00
github-actions[bot] 4a338477c3
Version Packages (#57)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-13 09:22:22 +02:00
Jonas Beck 00159f48ab
ci(release): remove yarn changeset tag that was moved into script (#56)
Signed-off-by: Jonas Beck <dev@jonasbeck.dk>
2025-06-13 09:11:42 +02:00
github-actions[bot] 40e077dac2
Version Packages (#55) 2025-06-12 15:47:48 +02:00
Jonas Beck ac24f2a916
ci: release (#54) 2025-06-12 15:32:33 +02:00
github-actions[bot] 727a8b3907
Version Packages (#53)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-12 15:04:22 +02:00
Jonas Beck 1cbf09ac35
ci(release): use yarn changeset tag after publish step (#52)
* ci(release): use yarn changeset tag for release

Signed-off-by: Jonas Beck <dev@jonasbeck.dk>

---------

Signed-off-by: Jonas Beck <dev@jonasbeck.dk>
2025-06-12 14:56:00 +02:00
59 changed files with 7005 additions and 1924 deletions

View File

@ -7,5 +7,5 @@
"access": "public",
"baseBranch": "origin/main",
"updateInternalDependencies": "patch",
"ignore": []
"ignore": ["app", "backend"]
}

View File

@ -0,0 +1,5 @@
---
'@kyverno/backstage-plugin-policy-reporter': patch
---
Add missing environment component to the `PolicyReportsPage`, which is now shown when no `kubernetes-cluster` Resources with the `kyverno.io/endpoint` annotation are defined.

View File

@ -43,7 +43,7 @@ jobs:
uses: changesets/action@e0145edc7d9d8679003495b11f87bd8ef63c0cba
with:
version: node .github/changeset-version.cjs
publish: yarn run publish
publish: yarn publish
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@ -83,3 +83,4 @@ dist-types
!.yarn/sdks
!.yarn/versions
/coverage

View File

@ -1,4 +1,6 @@
dist
dist-types
coverage
.github/changeset-version.cjs
.vscode
.yarnrc.yml

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,8 @@
nodeLinker: node-modules
plugins:
- checksum: 5e9fde8695ba2fd9e9e4bcc285a8817a491b6819bd1c8070cf55a34d8cff25b6da8af58d0a2e63be4dfcc38d883811a43aeb092b26a62f734458a4425bdda0b1
path: .yarn/plugins/@yarnpkg/plugin-backstage.cjs
spec: "https://versions.backstage.io/v1/releases/1.39.1/yarn-plugin"
yarnPath: .yarn/releases/yarn-4.5.0.cjs

View File

@ -6,79 +6,23 @@ organization:
name: My Company
backend:
# Used for enabling authentication, secret is shared by all backend plugins
# See https://backstage.io/docs/auth/service-to-service-auth for
# information on the format
# auth:
# keys:
# - secret: ${BACKEND_SECRET}
baseUrl: http://localhost:7007
# This is needed to test backend API using cURL locally
# auth:
# externalAccess:
# - type: static
# options:
# token: ${BACKSTAGE_TOKEN}
# subject: developmentToken
listen:
port: 7007
# Uncomment the following host directive to bind to specific interfaces
# host: 127.0.0.1
csp:
connect-src: ["'self'", 'http:', 'https:']
# Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference
# Default Helmet Content-Security-Policy values can be removed by setting the key to false
cors:
origin: http://localhost:3000
methods: [GET, HEAD, PATCH, POST, PUT, DELETE]
credentials: true
# This is for local development only, it is not recommended to use this in production
# The production database configuration is stored in app-config.production.yaml
database:
client: better-sqlite3
connection: ':memory:'
# workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir
# integrations:
# github:
# - host: github.com
# This is a Personal Access Token or PAT from GitHub. You can find out how to generate this token, and more information
# about setting up the GitHub integration here: https://backstage.io/docs/integrations/github/locations#configuration
# token: ${GITHUB_TOKEN}
### Example for how to add your GitHub Enterprise instance using the API:
# - host: ghe.example.net
# apiBaseUrl: https://ghe.example.net/api/v3
# token: ${GHE_TOKEN}
### Example for how to add a proxy endpoint for the frontend.
### A typical reason to do this is to handle HTTPS and CORS for internal services.
# endpoints:
# '/test':
# target: 'https://example.com'
# changeOrigin: true
# Reference documentation http://backstage.io/docs/features/techdocs/configuration
# Note: After experimenting with basic setup, use CI/CD to generate docs
# and an external cloud storage when deploying TechDocs for production use-case.
# https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach
techdocs:
builder: 'local' # Alternatives - 'external'
generator:
runIn: 'docker' # Alternatives - 'local'
publisher:
type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives.
auth:
# see https://backstage.io/docs/auth/ to learn about auth providers
providers:
# See https://backstage.io/docs/auth/guest/provider
guest: {}
scaffolder:
# see https://backstage.io/docs/features/software-templates/configuration for software template options
catalog:
rules:
- allow: [Component, System, API, Resource, Location]
@ -86,13 +30,3 @@ catalog:
# Local example data, file locations are relative to the backend process, typically `packages/backend`
- type: file
target: ../../catalog/entities.yaml
## Uncomment these lines to add more example data
##- type: url
##target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml
## Uncomment these lines to add an example org
##- type: url
## target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml
##rules:
## - allow: [User, Group]

View File

@ -1,3 +1,3 @@
{
"version": "1.38.1"
"version": "1.39.1"
}

View File

@ -30,3 +30,21 @@ spec:
type: database
owner: user:guest
---
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: policy-reporter
description: Policy-Reporter
annotations:
github.com/project-slug: kyverno/policy-reporter
kyverno.io/namespace: kyverno
kyverno.io/kind: Deployment,Pod
kyverno.io/resource-name: kyverno-background-controller
spec:
type: service
lifecycle: production
owner: user:guest
dependsOn:
- resource:default/dev
- resource:default/test
- resource:default/database

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

View File

@ -81,3 +81,70 @@ const serviceEntityPage = (
| policyDocumentationUrl | string | undefined | Optional URL used to generate links to policy documentation. [More information](/README.md#optional-custom-policy-documentation) |
</details>
<details>
<summary><strong>PolicyReportsPage</strong> - Sidebar component used to display all policy reports for a given environment</summary>
## PolicyReportsPage
The `PolicyReportsPage` component displays all policy reports for a given environment. This component shows reports from all sources and is intended to be placed on the Backstage sidebar.
> **Note:** This component is a work in progress. See https://github.com/kyverno/backstage-policy-reporter-plugin/issues/29 for current state of the component
### Screenshot
![EntityCustomPoliciesContent](./assets/policy-reports-page.PNG)
### Setup Steps
Add a new Route element with the path `/policy-reports` and element of `<PolicyReportsPage>` in `packages/app/src/App.tsx`
```diff
+ import { PolicyReportsPage } from '@kyverno/backstage-plugin-policy-reporter';
...
const routes = (
<FlatRoutes>
{/* existing routes... */}
+ <Route
+ path='/policy-reports'
+ element={<PolicyReportsPage title='My Optional Title' />}
+ />
</FlatRoutes>
);
```
Add a sidebar item that routes to the path setup in previous step
```diff
+import PolicyIcon from '@material-ui/icons/Policy';
export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
<SidebarLogo />
{/* existing sidebar items... */}
<SidebarScrollWrapper>
{/* existing sidebar items... */}
+ <SidebarItem icon={PolicyIcon} to='policy-reports' text='Policy Reports' />
</SidebarScrollWrapper>
</Sidebar>
</SidebarPage>
);
```
### Configuration Options
| Prop | Type | Default | Description |
| ---------------------- | ------ | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| title | string | Policy Reports | Optional Title to use for the content page |
| subtitle | string | View all policy reports from a Kubernetes cluster | Optional Subtitle to use for the content page |
| policyDocumentationUrl | string | undefined | Optional URL used to generate links to policy documentation. [More information](/README.md#optional-custom-policy-documentation) |
</details>

View File

@ -24,7 +24,7 @@
"prettier:check": "prettier --check .",
"new": "backstage-cli new --scope internal",
"prepare": "husky",
"publish": "yarn workspaces foreach --all --no-private --topological --verbose npm publish --tolerate-republish"
"publish": "yarn workspaces foreach --all --no-private --topological --verbose npm publish --tolerate-republish && yarn changeset tag"
},
"workspaces": {
"packages": [
@ -33,18 +33,19 @@
]
},
"dependencies": {
"@backstage/app-defaults": "^1.6.1",
"@backstage/app-defaults": "backstage:^",
"@changesets/cli": "^2.28.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.3.0"
"react-router-dom": "^6.30.1"
},
"devDependencies": {
"@backstage/cli": "^0.32.0",
"@backstage/e2e-test-utils": "^0.1.1",
"@backstage/cli": "backstage:^",
"@backstage/e2e-test-utils": "backstage:^",
"@backstage/repo-tools": "backstage:^",
"@playwright/test": "^1.32.3",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.18",
"husky": "^9.1.1",
"lerna": "^7.3.0",
"lint-staged": "^15.2.7",

9
packages/README.md Normal file
View File

@ -0,0 +1,9 @@
# The Packages Folder
This is where your own applications and centrally managed libraries live, each
in a separate folder of its own.
From the start there's an `app` folder (for the frontend) and a `backend` folder
(for the Node backend), but you can also add more modules in here that house
your core additions and adaptations, such as themes, common React component
libraries, utilities, and similar.

View File

@ -0,0 +1 @@
public

View File

@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);

73
packages/app/package.json Normal file
View File

@ -0,0 +1,73 @@
{
"name": "app",
"version": "0.0.0",
"private": true,
"bundled": true,
"repository": {
"type": "git",
"url": "https://github.com/kyverno/backstage-plugin-policy-reporter",
"directory": "packages/app"
},
"backstage": {
"role": "frontend"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"test": "backstage-cli package test",
"lint": "backstage-cli package lint"
},
"dependencies": {
"@backstage/app-defaults": "backstage:^",
"@backstage/catalog-model": "backstage:^",
"@backstage/cli": "backstage:^",
"@backstage/core-app-api": "backstage:^",
"@backstage/core-components": "backstage:^",
"@backstage/core-plugin-api": "backstage:^",
"@backstage/integration-react": "backstage:^",
"@backstage/plugin-api-docs": "backstage:^",
"@backstage/plugin-catalog": "backstage:^",
"@backstage/plugin-catalog-common": "backstage:^",
"@backstage/plugin-catalog-graph": "backstage:^",
"@backstage/plugin-catalog-import": "backstage:^",
"@backstage/plugin-catalog-react": "backstage:^",
"@backstage/plugin-permission-react": "backstage:^",
"@backstage/plugin-user-settings": "backstage:^",
"@backstage/theme": "backstage:^",
"@kyverno/backstage-plugin-policy-reporter": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"history": "^5.0.0",
"react": "^18.3.1",
"react-dom": "^18.0.2",
"react-router": "^6.30.1",
"react-router-dom": "^6.30.1",
"react-use": "^17.6.0"
},
"devDependencies": {
"@backstage/test-utils": "backstage:^",
"@playwright/test": "^1.32.3",
"@testing-library/dom": "^9.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.0.0",
"@types/react-dom": "*",
"cross-env": "^7.0.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"files": [
"dist"
]
}

View File

@ -0,0 +1,28 @@
import { render, waitFor } from '@testing-library/react';
import App from './App';
describe('App', () => {
it('should render', async () => {
process.env = {
NODE_ENV: 'test',
APP_CONFIG: [
{
data: {
app: { title: 'Test' },
backend: { baseUrl: 'http://localhost:7007' },
techdocs: {
storageUrl: 'http://localhost:7007/api/techdocs/static/docs',
},
},
context: 'test',
},
] as any,
};
const rendered = render(<App />);
await waitFor(() => {
expect(rendered.baseElement).toBeInTheDocument();
});
});
});

56
packages/app/src/App.tsx Normal file
View File

@ -0,0 +1,56 @@
import { Navigate, Route } from 'react-router-dom';
import { apiDocsPlugin } from '@backstage/plugin-api-docs';
import { CatalogEntityPage, CatalogIndexPage } from '@backstage/plugin-catalog';
import { catalogImportPlugin } from '@backstage/plugin-catalog-import';
import { UserSettingsPage } from '@backstage/plugin-user-settings';
import { apis } from './apis';
import { entityPage } from './components/catalog/EntityPage';
import { Root } from './components/Root';
import {
AlertDisplay,
OAuthRequestDialog,
SignInPage,
} from '@backstage/core-components';
import { createApp } from '@backstage/app-defaults';
import { AppRouter, FlatRoutes } from '@backstage/core-app-api';
import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';
import { PolicyReportsPage } from '@kyverno/backstage-plugin-policy-reporter';
const app = createApp({
apis,
bindRoutes({ bind }) {
bind(apiDocsPlugin.externalRoutes, {
registerApi: catalogImportPlugin.routes.importPage,
});
},
components: {
SignInPage: props => <SignInPage {...props} auto providers={['guest']} />,
},
});
const routes = (
<FlatRoutes>
<Route path="/" element={<Navigate to="catalog" />} />
<Route path="/catalog" element={<CatalogIndexPage />} />
<Route
path="/catalog/:namespace/:kind/:name"
element={<CatalogEntityPage />}
>
{entityPage}
</Route>
<Route path="/kyverno" element={<PolicyReportsPage />} />
<Route path="/settings" element={<UserSettingsPage />} />
<Route path="/catalog-graph" element={<CatalogGraphPage />} />
</FlatRoutes>
);
export default app.createRoot(
<>
<AlertDisplay />
<OAuthRequestDialog />
<AppRouter>
<Root>{routes}</Root>
</AppRouter>
</>,
);

19
packages/app/src/apis.ts Normal file
View File

@ -0,0 +1,19 @@
import {
ScmIntegrationsApi,
scmIntegrationsApiRef,
ScmAuth,
} from '@backstage/integration-react';
import {
AnyApiFactory,
configApiRef,
createApiFactory,
} from '@backstage/core-plugin-api';
export const apis: AnyApiFactory[] = [
createApiFactory({
api: scmIntegrationsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
ScmAuth.createDefaultApiFactory(),
];

View File

@ -0,0 +1,45 @@
import { PropsWithChildren } from 'react';
import {
Settings as SidebarSettings,
UserSettingsSignInAvatar,
} from '@backstage/plugin-user-settings';
import {
Sidebar,
SidebarDivider,
SidebarGroup,
SidebarItem,
SidebarPage,
SidebarSpace,
} from '@backstage/core-components';
import HomeIcon from '@material-ui/icons/Home';
import MenuIcon from '@material-ui/icons/Menu';
import AssessmentIcon from '@material-ui/icons/Assessment';
export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
<SidebarDivider />
<SidebarGroup label="Menu" icon={<MenuIcon />}>
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
{/* End global nav */}
<SidebarDivider />
<SidebarItem
icon={AssessmentIcon}
to="kyverno"
text="Policy Reporter"
/>
</SidebarGroup>
<SidebarSpace />
<SidebarDivider />
<SidebarGroup
label="Settings"
icon={<UserSettingsSignInAvatar />}
to="/settings"
>
<SidebarSettings />
</SidebarGroup>
</Sidebar>
{children}
</SidebarPage>
);

View File

@ -0,0 +1 @@
export { Root } from './Root';

View File

@ -0,0 +1,90 @@
import { Grid } from '@material-ui/core';
import {
EntityAboutCard,
EntityLayout,
EntityOrphanWarning,
EntityProcessingErrorsPanel,
EntityRelationWarning,
EntitySwitch,
hasCatalogProcessingErrors,
hasRelationWarnings,
isComponentType,
isKind,
isOrphan,
} from '@backstage/plugin-catalog';
import { EntityKyvernoPoliciesContent } from '@kyverno/backstage-plugin-policy-reporter';
const entityWarningContent = (
<>
<EntitySwitch>
<EntitySwitch.Case if={isOrphan}>
<Grid item xs={12}>
<EntityOrphanWarning />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={hasRelationWarnings}>
<Grid item xs={12}>
<EntityRelationWarning />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={hasCatalogProcessingErrors}>
<Grid item xs={12}>
<EntityProcessingErrorsPanel />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
</>
);
const overviewContent = (
<Grid container spacing={3} alignItems="stretch">
{entityWarningContent}
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
</Grid>
);
const serviceEntityPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/kyverno" title="Kyverno Policies">
<EntityKyvernoPoliciesContent />
</EntityLayout.Route>
</EntityLayout>
);
const defaultEntityPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
</EntityLayout>
);
const componentPage = (
<EntitySwitch>
<EntitySwitch.Case if={isComponentType('service')}>
{serviceEntityPage}
</EntitySwitch.Case>
<EntitySwitch.Case>{defaultEntityPage}</EntitySwitch.Case>
</EntitySwitch>
);
export const entityPage = (
<EntitySwitch>
<EntitySwitch.Case if={isKind('component')} children={componentPage} />
<EntitySwitch.Case>{defaultEntityPage}</EntitySwitch.Case>
</EntitySwitch>
);

View File

@ -0,0 +1,5 @@
import '@backstage/cli/asset-types';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);

View File

@ -0,0 +1 @@
import '@testing-library/jest-dom';

View File

@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);

View File

@ -0,0 +1,50 @@
{
"name": "backend",
"version": "0.0.11",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/backstage/community-plugins",
"directory": "workspaces/sonarqube/packages/backend"
},
"backstage": {
"role": "backend"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"build-image": "docker build ../.. -f Dockerfile --tag backstage"
},
"dependencies": {
"@backstage/backend-defaults": "backstage:^",
"@backstage/backend-plugin-api": "backstage:^",
"@backstage/config": "backstage:^",
"@backstage/plugin-app-backend": "backstage:^",
"@backstage/plugin-auth-backend": "backstage:^",
"@backstage/plugin-auth-backend-module-guest-provider": "backstage:^",
"@backstage/plugin-auth-node": "backstage:^",
"@backstage/plugin-catalog-backend": "backstage:^",
"@kyverno/backstage-plugin-policy-reporter-backend": "workspace:^",
"app": "link:../app",
"better-sqlite3": "^9.0.0",
"dockerode": "^3.3.1",
"node-gyp": "^9.0.0",
"pg": "^8.11.3",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "backstage:^",
"@types/dockerode": "^3.3.0",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
"@types/luxon": "^2.0.4"
},
"files": [
"dist"
]
}

View File

@ -0,0 +1,14 @@
import { createBackend } from '@backstage/backend-defaults';
const backend = createBackend();
backend.add(import('@backstage/plugin-app-backend'));
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
backend.add(import('@backstage/plugin-catalog-backend'));
backend.add(import('@kyverno/backstage-plugin-policy-reporter-backend'));
backend.start();

View File

@ -1,5 +1,21 @@
# @kyverno/backstage-plugin-policy-reporter-backend
## 2.1.4
### Patch Changes
- 00159f4: New release to validate updated publishing workflow
- Updated dependencies [00159f4]
- @kyverno/backstage-plugin-policy-reporter-common@2.0.7
## 2.1.3
### Patch Changes
- ac24f2a: New release to validate updated publishing workflow
- Updated dependencies [ac24f2a]
- @kyverno/backstage-plugin-policy-reporter-common@2.0.6
## 2.1.2
### Patch Changes

View File

@ -3,8 +3,8 @@ import { createBackend } from '@backstage/backend-defaults';
const backend = createBackend();
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
backend.add(import('@backstage/plugin-catalog-backend'));
backend.add(import('../src'));
backend.start();

View File

@ -1,6 +1,6 @@
{
"name": "@kyverno/backstage-plugin-policy-reporter-backend",
"version": "2.1.2",
"version": "2.1.4",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@ -38,11 +38,11 @@
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/backend-defaults": "^0.9.0",
"@backstage/backend-plugin-api": "^1.3.0",
"@backstage/config": "^1.3.2",
"@backstage/plugin-catalog-node": "^1.16.3",
"@kyverno/backstage-plugin-policy-reporter-common": "2.0.5",
"@backstage/backend-defaults": "backstage:^",
"@backstage/backend-plugin-api": "backstage:^",
"@backstage/config": "backstage:^",
"@backstage/plugin-catalog-node": "backstage:^",
"@kyverno/backstage-plugin-policy-reporter-common": "workspace:^",
"@types/express": "*",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
@ -50,12 +50,12 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/backend-test-utils": "^1.4.0",
"@backstage/catalog-model": "^1.7.3",
"@backstage/cli": "^0.32.0",
"@backstage/plugin-auth-backend": "^0.24.5",
"@backstage/plugin-auth-backend-module-guest-provider": "^0.2.7",
"@backstage/plugin-catalog-backend": "^1.32.1",
"@backstage/backend-test-utils": "backstage:^",
"@backstage/catalog-model": "backstage:^",
"@backstage/cli": "backstage:^",
"@backstage/plugin-auth-backend": "backstage:^",
"@backstage/plugin-auth-backend-module-guest-provider": "backstage:^",
"@backstage/plugin-catalog-backend": "backstage:^",
"@types/supertest": "^2.0.12",
"better-sqlite3": "^9.0.0",
"msw": "^1.0.0",

View File

@ -1,5 +1,17 @@
# @kyverno/backstage-plugin-policy-reporter-common
## 2.0.7
### Patch Changes
- 00159f4: New release to validate updated publishing workflow
## 2.0.6
### Patch Changes
- ac24f2a: New release to validate updated publishing workflow
## 2.0.5
### Patch Changes

View File

@ -1,7 +1,7 @@
{
"name": "@kyverno/backstage-plugin-policy-reporter-common",
"description": "Common functionalities for the policy-reporter plugin",
"version": "2.0.5",
"version": "2.0.7",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@ -40,7 +40,7 @@
"postpack": "backstage-cli package postpack"
},
"devDependencies": {
"@backstage/cli": "^0.32.0"
"@backstage/cli": "backstage:^"
},
"files": [
"dist"

View File

@ -1,5 +1,39 @@
# @kyverno/backstage-plugin-policy-reporter
## 2.4.0
### Minor Changes
- 0d6cf4b: Add Status and Severity filter to `PolicyReportsPage` component and updates the UI to now be 1 big table that by default show all failing policies
### Patch Changes
- 0d6cf4b: Update the `PolicyReportsPage` component's exported name to match the documentation. It was previously set to `PolicyReporterPage` by mistake and has now been corrected to `PolicyReportsPage`.
## 2.3.0
### Minor Changes
- 274db4d: Add initial implemenation for the `PolicyReportsPage` component that can be used for a more global overview of all policy reports for a given kubernetes cluster.
Existing Entity components previously included a search bar on all tables, but it only supported searching within the already displayed policy reports. This version removes the search bar to avoid confusion and improve clarity.
## 2.2.4
### Patch Changes
- 00159f4: New release to validate updated publishing workflow
- Updated dependencies [00159f4]
- @kyverno/backstage-plugin-policy-reporter-common@2.0.7
## 2.2.3
### Patch Changes
- ac24f2a: New release to validate updated publishing workflow
- Updated dependencies [ac24f2a]
- @kyverno/backstage-plugin-policy-reporter-common@2.0.6
## 2.2.2
### Patch Changes

View File

@ -12,6 +12,7 @@ import {
fetchApiRef,
} from '@backstage/core-plugin-api';
import { CatalogClient } from '@backstage/catalog-client';
import { PolicyReportsPage } from '../src/components/PolicyReportsPage';
const mockEntity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
@ -176,4 +177,10 @@ createDevApp()
</EntityProvider>
),
})
.addPage({
path: '/policyreporter-page',
title: 'Policy Reporter Page',
// Wrap the plugin in entity mock
element: <PolicyReportsPage />,
})
.render();

View File

@ -1,6 +1,6 @@
{
"name": "@kyverno/backstage-plugin-policy-reporter",
"version": "2.2.2",
"version": "2.4.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@ -39,26 +39,26 @@
"directory": "plugins/policy-reporter"
},
"dependencies": {
"@backstage/catalog-client": "^1.9.1",
"@backstage/catalog-model": "^1.7.3",
"@backstage/core-components": "^0.17.1",
"@backstage/core-plugin-api": "^1.10.6",
"@backstage/plugin-catalog-react": "^1.17.0",
"@backstage/theme": "^0.6.5",
"@kyverno/backstage-plugin-policy-reporter-common": "2.0.5",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@backstage/catalog-client": "backstage:^",
"@backstage/catalog-model": "backstage:^",
"@backstage/core-components": "backstage:^",
"@backstage/core-plugin-api": "backstage:^",
"@backstage/plugin-catalog-react": "backstage:^",
"@backstage/theme": "backstage:^",
"@kyverno/backstage-plugin-policy-reporter-common": "workspace:^",
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3",
"@material-ui/lab": "4.0.0-alpha.61",
"react-use": "^17.2.4"
"react-use": "^17.6.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.32.0",
"@backstage/core-app-api": "^1.16.1",
"@backstage/dev-utils": "^1.1.9",
"@backstage/test-utils": "^1.7.7",
"@backstage/cli": "backstage:^",
"@backstage/core-app-api": "backstage:^",
"@backstage/dev-utils": "backstage:^",
"@backstage/test-utils": "backstage:^",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.0.0",

View File

@ -108,6 +108,7 @@ export const EntityCustomPoliciesContent = ({
title="Failing Policy Results"
emptyContentText="No failing policies"
policyDocumentationUrl={policyDocumentationUrl}
enableSearch={false}
/>
</Grid>
<Grid item xs={12}>
@ -123,6 +124,7 @@ export const EntityCustomPoliciesContent = ({
title="Passing Policy Results"
emptyContentText="No passing policies"
policyDocumentationUrl={policyDocumentationUrl}
enableSearch={false}
/>
</Grid>
<Grid item xs={12}>
@ -138,6 +140,7 @@ export const EntityCustomPoliciesContent = ({
title="Skipped Policy Results"
emptyContentText="No skipped policies"
policyDocumentationUrl={policyDocumentationUrl}
enableSearch={false}
/>
</Grid>
</Grid>

View File

@ -104,6 +104,7 @@ export const EntityKyvernoPoliciesContent = ({
title="Failing Policy Results"
emptyContentText="No failing policies"
policyDocumentationUrl={policyDocumentationUrl}
enableSearch={false}
/>
</Grid>
<Grid item xs={12}>
@ -119,6 +120,7 @@ export const EntityKyvernoPoliciesContent = ({
title="Passing Policy Results"
emptyContentText="No passing policies"
policyDocumentationUrl={policyDocumentationUrl}
enableSearch={false}
/>
</Grid>
<Grid item xs={12}>
@ -134,6 +136,7 @@ export const EntityKyvernoPoliciesContent = ({
title="Skipped Policy Results"
emptyContentText="No skipped policies"
policyDocumentationUrl={policyDocumentationUrl}
enableSearch={false}
/>
</Grid>
</Grid>

View File

@ -0,0 +1,35 @@
import { Entity } from '@backstage/catalog-model';
import { MissingEnvironmentsEmptyState } from './MissingEnvironmentsEmptyState';
import { renderInTestApp } from '@backstage/test-utils';
describe('MissingEnvironmentsEmptyState', () => {
it('should render empty state component with undefined entity', async () => {
const extension = await renderInTestApp(<MissingEnvironmentsEmptyState />);
expect(
extension.getAllByText('Missing Valid Environment Dependency'),
).toBeTruthy();
});
it('should render empty state component with entity', async () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'custom-resource',
metadata: {
name: 'service-name',
},
spec: {
type: 'custom-type',
owner: 'user:namespace/name',
},
};
const extension = await renderInTestApp(
<MissingEnvironmentsEmptyState entity={entity} />,
);
expect(
extension.getAllByText('Missing Valid Environment Dependency'),
).toBeTruthy();
});
});

View File

@ -25,4 +25,42 @@ describe('PolicyReportsDrawerComponent', () => {
expect(extension.getAllByText('Policy1')).toHaveLength(1);
});
it('should render table displaying the policy details with additonal properties', async () => {
const data: ListResult = {
id: '0',
kind: 'deployment',
namespace: 'default',
name: 'name',
resourceId: 'id',
message: 'Policy 1 passed successfully',
timestamp: 123456789,
policy: 'Policy1',
rule: 'Rule1',
status: 'pass',
severity: 'low',
properties: {
key: 'value',
},
};
const extension = await renderInTestApp(
<PolicyReportsDrawerComponent content={data} />,
);
expect(extension.getAllByText('Policy1')).toBeTruthy();
expect(extension.getAllByText('key')).toBeTruthy();
expect(extension.getAllByText('value')).toBeTruthy();
expect(extension.getAllByText('Additional Properties')).toBeTruthy();
});
it('should render message if policy is undefined', async () => {
const extension = await renderInTestApp(
<PolicyReportsDrawerComponent content={undefined} />,
);
expect(
extension.getAllByText('No policy information available'),
).toBeTruthy();
});
});

View File

@ -9,6 +9,7 @@ import {
Box,
Paper,
makeStyles,
GridProps,
} from '@material-ui/core';
import { SeverityComponent } from '../SeverityComponent';
import { StatusComponent } from '../StatusComponent';
@ -21,6 +22,7 @@ interface PolicyReportsDrawerProps {
const useStyles = makeStyles(theme => ({
root: {
marginBottom: theme.spacing(2),
maxWidth: 800,
},
sectionTitle: {
marginBottom: theme.spacing(1),
@ -63,6 +65,33 @@ const useStyles = makeStyles(theme => ({
},
}));
interface GridItemProps extends GridProps {
label: string;
value: string | React.ReactNode;
}
const GridItem = ({ label, value, ...gridProps }: GridItemProps) => {
const classes = useStyles();
const isString = typeof value === 'string';
return (
<Grid item {...gridProps}>
<Box className={classes.propertyContainer}>
<Typography variant="body2" className={classes.propertyLabel}>
{label}
</Typography>
<Typography
variant="body2"
className={classes.propertyValue}
component={isString ? 'p' : 'div'}
>
{value}
</Typography>
</Box>
</Grid>
);
};
export const PolicyReportsDrawerComponent = ({
content,
}: PolicyReportsDrawerProps) => {
@ -92,66 +121,16 @@ export const PolicyReportsDrawerComponent = ({
General Information
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Box className={classes.propertyContainer}>
<Typography variant="body2" className={classes.propertyLabel}>
ID
</Typography>
<Typography variant="body2" className={classes.propertyValue}>
{content.id}
</Typography>
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box className={classes.propertyContainer}>
<Typography variant="body2" className={classes.propertyLabel}>
Name
</Typography>
<Typography variant="body2" className={classes.propertyValue}>
{content.name}
</Typography>
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box className={classes.propertyContainer}>
<Typography variant="body2" className={classes.propertyLabel}>
Namespace
</Typography>
<Typography variant="body2" className={classes.propertyValue}>
{content.namespace}
</Typography>
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box className={classes.propertyContainer}>
<Typography variant="body2" className={classes.propertyLabel}>
Kind
</Typography>
<Typography variant="body2" className={classes.propertyValue}>
{content.kind}
</Typography>
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box className={classes.propertyContainer}>
<Typography variant="body2" className={classes.propertyLabel}>
Resource ID
</Typography>
<Typography variant="body2" className={classes.propertyValue}>
{content.resourceId}
</Typography>
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box className={classes.propertyContainer}>
<Typography variant="body2" className={classes.propertyLabel}>
Timestamp
</Typography>
<Typography variant="body2" className={classes.propertyValue}>
{formattedDate}
</Typography>
</Box>
</Grid>
{[
{ label: 'ID', value: content.id },
{ label: 'Name', value: content.name },
{ label: 'Namespace', value: content.namespace },
{ label: 'Kind', value: content.kind },
{ label: 'Resource ID', value: content.resourceId },
{ label: 'Timestamp', value: formattedDate },
].map(({ label, value }, index) => (
<GridItem label={label} key={index} value={value} xs={12} md={6} />
))}
</Grid>
<Divider className={classes.divider} />
@ -161,22 +140,18 @@ export const PolicyReportsDrawerComponent = ({
Status Information
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Box className={classes.propertyContainer}>
<Typography variant="body2" className={classes.propertyLabel}>
Status
</Typography>
<StatusComponent status={content.status} />
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box className={classes.propertyContainer}>
<Typography variant="body2" className={classes.propertyLabel}>
Severity
</Typography>
<SeverityComponent severity={content.severity} />
</Box>
</Grid>
<GridItem
label="Status"
value={<StatusComponent status={content.status} />}
xs={12}
md={6}
/>
<GridItem
label="Severity"
value={<SeverityComponent severity={content.severity} />}
xs={12}
md={6}
/>
</Grid>
<Divider className={classes.divider} />
@ -186,26 +161,8 @@ export const PolicyReportsDrawerComponent = ({
Policy Information
</Typography>
<Grid container spacing={2}>
<Grid item xs={12}>
<Box className={classes.propertyContainer}>
<Typography variant="body2" className={classes.propertyLabel}>
Policy
</Typography>
<Typography variant="body2" className={classes.propertyValue}>
{content.policy}
</Typography>
</Box>
</Grid>
<Grid item xs={12}>
<Box className={classes.propertyContainer}>
<Typography variant="body2" className={classes.propertyLabel}>
Rule
</Typography>
<Typography variant="body2" className={classes.propertyValue}>
{content.rule}
</Typography>
</Box>
</Grid>
<GridItem label="Policy" value={content.policy} xs={12} />
<GridItem label="Rule" value={content.rule} xs={12} />
</Grid>
<Divider className={classes.divider} />

View File

@ -0,0 +1 @@
export { PolicyReportsDrawerComponent } from './PolicyReportsDrawerComponent';

View File

@ -0,0 +1,58 @@
import { policyReporterApiRef } from '../../api';
import { TestApiProvider, renderInTestApp } from '@backstage/test-utils';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { PolicyReportsPage } from './PolicyReportsPage';
const mockPolicyReportApiRef = {
namespacedResults: jest.fn(),
};
const mockCatalogApiRef = {
getEntities: jest.fn(),
};
describe('EntityKyvernoPolicyReportsContent component', () => {
it('should not render when kubernetes-cluster resources are missing', async () => {
// Act
const extension = await renderInTestApp(
<TestApiProvider
apis={[
[policyReporterApiRef, mockPolicyReportApiRef as any],
[catalogApiRef, mockCatalogApiRef],
]}
>
<PolicyReportsPage />,
</TestApiProvider>,
);
// Assert
expect(
extension.getByText('No kubernetes-cluster Resources found'),
).toBeTruthy();
expect(extension.getByText('Policy Reports')).toBeTruthy();
});
it('should render PolicyReportsTable if environments are valid', async () => {
// Arrange
mockCatalogApiRef.getEntities.mockImplementationOnce(() => {
return Promise.resolve({ items: [{ metadata: { name: 'dev' } }] });
});
// Act
const extension = await renderInTestApp(
<TestApiProvider
apis={[
[policyReporterApiRef, mockPolicyReportApiRef as any],
[catalogApiRef, mockCatalogApiRef],
]}
>
<PolicyReportsPage />
</TestApiProvider>,
);
// Assert
expect(extension.getByText('Policy Reports')).toBeTruthy();
expect(extension.getByText('Policy Results')).toBeTruthy();
});
});

View File

@ -0,0 +1,112 @@
import {
Content,
ContentHeader,
EmptyState,
Header,
Link,
Page,
Progress,
} from '@backstage/core-components';
import { useEnvironments } from '../../hooks/useEnvironments';
import { SelectEnvironment } from '../SelectEnvironment';
import { Button, Grid } from '@material-ui/core';
import { PolicyReportsTable } from '../PolicyReportsTable';
import { useState } from 'react';
import {
Severity,
Status,
} from '@kyverno/backstage-plugin-policy-reporter-common';
import { SelectStatus } from '../SelectStatus';
import { SelectSeverity } from '../SelectSeverity';
export interface PolicyReportsPageProps {
title?: string;
policyDocumentationUrl?: string;
subtitle?: string;
}
export const PolicyReportsPage = ({
title = 'Policy Reports',
subtitle = 'View all policy reports from a Kubernetes cluster',
policyDocumentationUrl,
}: PolicyReportsPageProps) => {
const {
environments,
environmentsLoading,
setCurrentEnvironment,
currentEnvironment,
} = useEnvironments();
const [status, setStatus] = useState<Status[]>(['fail']);
const [severity, setSeverity] = useState<Severity[]>([]);
// Fetching environments
if (environmentsLoading) return <Progress />;
// Environments missing
if (environments === undefined || !currentEnvironment)
return (
<Page themeId="tool">
<Header title={title} subtitle={subtitle} />
<Content>
<EmptyState
missing="content"
title="No kubernetes-cluster Resources found"
description={
<>
You need to define a Resource with the kubernetes-cluster type
and a<code> kyverno.io/endpoint </code> annotation for this
plugin to work.
</>
}
action={
<>
<Button
color="primary"
component={Link}
to="https://github.com/kyverno/backstage-policy-reporter-plugin"
>
Read More
</Button>
</>
}
/>
</Content>
</Page>
);
return (
<Page themeId="tool">
<Header title={title} subtitle={subtitle} />
<Content>
<ContentHeader>
<SelectStatus currentStatus={status} setStatus={setStatus} />
<SelectSeverity
currentSeverity={severity}
setSeverity={setSeverity}
/>
<SelectEnvironment
environments={environments}
currentEnvironment={currentEnvironment}
setCurrentEnvironment={setCurrentEnvironment}
/>
</ContentHeader>
<Grid container spacing={2}>
<Grid item xs={12}>
<PolicyReportsTable
currentEnvironment={currentEnvironment}
filter={{
status: status,
severities: severity,
}}
title="Policy Results"
emptyContentText="No policies found"
policyDocumentationUrl={policyDocumentationUrl}
pagination={{ offset: 25 }}
/>
</Grid>
</Grid>
</Content>
</Page>
);
};

View File

@ -0,0 +1 @@
export { PolicyReportsPage } from './PolicyReportsPage';

View File

@ -3,10 +3,11 @@ import {
Table,
TableColumn,
} from '@backstage/core-components';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import {
Filter,
ListResult,
Pagination,
} from '@kyverno/backstage-plugin-policy-reporter-common';
import { Drawer, makeStyles } from '@material-ui/core';
import Chip from '@material-ui/core/Chip';
@ -15,7 +16,7 @@ import { SeverityComponent } from '../SeverityComponent';
import Launch from '@material-ui/icons/Launch';
import { Environment } from '@kyverno/backstage-plugin-policy-reporter-common';
import { usePaginatedPolicies } from '../../hooks/usePaginatedPolicies';
import { PolicyReportsDrawerComponent } from '../PolicyReportsDrawerComponent/PolicyReportsDrawerComponent';
import { PolicyReportsDrawerComponent } from '../PolicyReportsDrawerComponent';
interface PolicyReportsTableProps {
currentEnvironment: Environment;
@ -23,6 +24,9 @@ interface PolicyReportsTableProps {
title: string;
emptyContentText: string;
policyDocumentationUrl?: string;
enableSearch?: boolean;
pagination?: Partial<Pagination>;
pageSizeOptions?: number[];
}
export const PolicyReportsTable = ({
@ -31,6 +35,9 @@ export const PolicyReportsTable = ({
currentEnvironment,
filter,
policyDocumentationUrl,
enableSearch,
pagination,
pageSizeOptions,
}: PolicyReportsTableProps) => {
const useStyles = makeStyles(theme => ({
empty: {
@ -40,6 +47,15 @@ export const PolicyReportsTable = ({
},
}));
const [search, setSearch] = useState<string | undefined>(undefined);
const mergedFilter = useMemo(
() => ({
...filter,
search: search ?? filter.search, // override search if search state is defined
}),
[filter, search],
);
const [showDrawer, setShowDrawer] = useState<boolean>(false);
const [drawerContent, setDrawerContent] = useState<ListResult | undefined>(
undefined,
@ -108,10 +124,11 @@ export const PolicyReportsTable = ({
policies,
policiesError,
currentPage,
currentOffset,
setCurrentPage,
setCurrentOffset,
initialLoading,
} = usePaginatedPolicies(currentEnvironment, filter);
} = usePaginatedPolicies(currentEnvironment, mergedFilter, pagination);
if (policiesError) return <ResponseErrorPanel error={policiesError} />;
@ -140,6 +157,9 @@ export const PolicyReportsTable = ({
options={{
sorting: true,
padding: 'dense',
search: enableSearch,
pageSize: currentOffset,
pageSizeOptions: pageSizeOptions,
}}
onRowClick={(
event?: React.MouseEvent<Element, MouseEvent>,
@ -157,6 +177,7 @@ export const PolicyReportsTable = ({
onRowsPerPageChange={page => setCurrentOffset(page)}
onPageChange={page => setCurrentPage(page)}
emptyContent={<div className={classes.empty}>{emptyContentText}</div>}
onSearchChange={searchText => setSearch(searchText)}
/>
</>
);

View File

@ -0,0 +1,36 @@
import { renderInTestApp } from '@backstage/test-utils';
import { SelectSeverity } from './SelectSeverity';
const setCurrentSeverity = jest.fn();
describe('SelectSeverity', () => {
it('should render the selected environment', async () => {
const extension = await renderInTestApp(
<SelectSeverity
currentSeverity={['critical']}
setSeverity={setCurrentSeverity}
/>,
);
expect(extension.getByText('critical')).toBeTruthy();
expect(extension.getByText('Severity')).toBeTruthy();
});
it('should render the selected environments', async () => {
const extension = await renderInTestApp(
<SelectSeverity
currentSeverity={['info', 'low']}
setSeverity={setCurrentSeverity}
/>,
);
expect(extension.getByText('info, low')).toBeTruthy();
expect(extension.getByText('Severity')).toBeTruthy();
});
it('should render all when nothing is selected', async () => {
const extension = await renderInTestApp(
<SelectSeverity currentSeverity={[]} setSeverity={setCurrentSeverity} />,
);
expect(extension.getByText('All')).toBeTruthy();
expect(extension.getByText('Severity')).toBeTruthy();
});
});

View File

@ -0,0 +1,70 @@
import MenuItem from '@material-ui/core/MenuItem';
import InputLabel from '@material-ui/core/InputLabel';
import FormControl from '@material-ui/core/FormControl';
import Select from '@material-ui/core/Select';
import { Severity } from '@kyverno/backstage-plugin-policy-reporter-common';
import { makeStyles } from '@material-ui/core/styles';
import { Checkbox, ListItemText } from '@material-ui/core';
// This could be moved into the common package if needed in multiple places
const SEVERITY_VALUES: Severity[] = [
'unknown',
'low',
'medium',
'high',
'critical',
'info',
];
const useStyles = makeStyles({
formControl: {
margin: 8,
minWidth: 150,
},
});
export type SelectSeverityProps = {
currentSeverity: Severity[];
setSeverity: (Status: Severity[]) => void;
};
export const SelectSeverity = ({
currentSeverity: currentSeverity,
setSeverity: setSeverity,
}: SelectSeverityProps) => {
const classes = useStyles();
const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
setSeverity(event.target.value as Severity[]);
};
return (
<FormControl className={classes.formControl}>
<InputLabel id="select-severity-label" shrink>
Severity
</InputLabel>
<Select
labelId="select-severity-label"
id="select-severity"
multiple
displayEmpty
value={currentSeverity}
renderValue={selected => {
if ((selected as Severity[]).length === 0) {
return 'All';
}
return (selected as Severity[]).join(', ');
}}
onChange={handleChange}
>
{SEVERITY_VALUES.map(severity => (
<MenuItem key={severity} value={severity}>
<Checkbox checked={currentSeverity.includes(severity)} />
<ListItemText primary={severity} />
</MenuItem>
))}
</Select>
</FormControl>
);
};

View File

@ -0,0 +1 @@
export { SelectSeverity, type SelectSeverityProps } from './SelectSeverity';

View File

@ -0,0 +1,33 @@
import { renderInTestApp } from '@backstage/test-utils';
import { SelectStatus } from './SelectStatus';
const setCurrentStatus = jest.fn();
describe('SelectStatus', () => {
it('should render the selected environment', async () => {
const extension = await renderInTestApp(
<SelectStatus currentStatus={['fail']} setStatus={setCurrentStatus} />,
);
expect(extension.getByText('fail')).toBeTruthy();
expect(extension.getByText('Status')).toBeTruthy();
});
it('should render the selected environments', async () => {
const extension = await renderInTestApp(
<SelectStatus
currentStatus={['fail', 'summary']}
setStatus={setCurrentStatus}
/>,
);
expect(extension.getByText('fail, summary')).toBeTruthy();
expect(extension.getByText('Status')).toBeTruthy();
});
it('should render all when nothing is selected', async () => {
const extension = await renderInTestApp(
<SelectStatus currentStatus={[]} setStatus={setCurrentStatus} />,
);
expect(extension.getByText('All')).toBeTruthy();
expect(extension.getByText('Status')).toBeTruthy();
});
});

View File

@ -0,0 +1,70 @@
import MenuItem from '@material-ui/core/MenuItem';
import InputLabel from '@material-ui/core/InputLabel';
import FormControl from '@material-ui/core/FormControl';
import Select from '@material-ui/core/Select';
import { Status } from '@kyverno/backstage-plugin-policy-reporter-common';
import { makeStyles } from '@material-ui/core/styles';
import { Checkbox, ListItemText } from '@material-ui/core';
// This could be moved into the common package if needed in multiple places
const STATUS_VALUES: Status[] = [
'fail',
'skip',
'pass',
'warn',
'error',
'summary',
];
const useStyles = makeStyles({
formControl: {
margin: 8,
minWidth: 150,
},
});
export type SelectStatusProps = {
currentStatus: Status[];
setStatus: (Status: Status[]) => void;
};
export const SelectStatus = ({
currentStatus,
setStatus,
}: SelectStatusProps) => {
const classes = useStyles();
const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
setStatus(event.target.value as Status[]);
};
return (
<FormControl className={classes.formControl}>
<InputLabel id="select-status-label" shrink>
Status
</InputLabel>
<Select
labelId="select-status-label"
id="select-status"
multiple
displayEmpty
value={currentStatus}
renderValue={selected => {
if ((selected as Status[]).length === 0) {
return 'All';
}
return (selected as Status[]).join(', ');
}}
onChange={handleChange}
>
{STATUS_VALUES.map(status => (
<MenuItem key={status} value={status}>
<Checkbox checked={currentStatus.includes(status)} />
<ListItemText primary={status} />
</MenuItem>
))}
</Select>
</FormControl>
);
};

View File

@ -0,0 +1 @@
export { SelectStatus, type SelectStatusProps } from './SelectStatus';

View File

@ -0,0 +1,45 @@
import { renderHook, waitFor } from '@testing-library/react';
import { useApi } from '@backstage/core-plugin-api';
import { useEnvironments } from './useEnvironments';
import { Environment } from '@kyverno/backstage-plugin-policy-reporter-common';
jest.mock('@backstage/core-plugin-api');
describe('useEnvironments', () => {
const mockGetEntities = jest.fn();
afterEach(() => {
jest.resetAllMocks();
});
it('should return environments', async () => {
(useApi as any).mockReturnValue({
getEntities: mockGetEntities.mockResolvedValue({
items: [
{ metadata: { name: 'dev', namespace: 'default' }, kind: 'resource' },
],
}),
});
const expected: Environment[] = [
{
name: 'dev',
entityRef: 'resource:default/dev',
id: 0,
},
];
const { result } = renderHook(() => useEnvironments());
expect(result.current.environmentsLoading).toEqual(true);
await waitFor(() => {
expect(result.current.setCurrentEnvironment).toBeDefined();
expect(result.current.environmentsLoading).toEqual(false);
expect(result.current.environments).toStrictEqual(expected);
expect(result.current.environments).toContainEqual(
result.current.currentEnvironment,
);
});
});
});

View File

@ -0,0 +1,49 @@
import { useState } from 'react';
import { useAsync } from 'react-use';
import { Environment } from '@kyverno/backstage-plugin-policy-reporter-common';
import {
CATALOG_FILTER_EXISTS,
catalogApiRef,
} from '@backstage/plugin-catalog-react';
import { useApi } from '@backstage/core-plugin-api';
export const useEnvironments = () => {
const catalogApi = useApi(catalogApiRef);
const [currentEnvironment, setCurrentEnvironment] = useState<
Environment | undefined
>(undefined);
const { value: environments, loading: environmentsLoading } = useAsync(
async (): Promise<Environment[] | undefined> => {
const entities = await catalogApi.getEntities({
fields: ['metadata.name', 'metadata.namespace', 'kind'],
filter: {
kind: 'Resource',
'spec.type': 'kubernetes-cluster',
'metadata.annotations.kyverno.io/endpoint': CATALOG_FILTER_EXISTS,
},
});
if (!entities) return undefined;
const environmentList: Environment[] = entities.items.map(
(entity, index) => ({
id: index,
entityRef: `${entity.kind}:${entity.metadata.namespace}/${entity.metadata.name}`,
name: entity.metadata.name,
}),
);
setCurrentEnvironment(environmentList[0]);
return environmentList;
},
);
return {
environments,
environmentsLoading,
currentEnvironment,
setCurrentEnvironment,
};
};

View File

@ -1,5 +1,8 @@
import { useAsync } from 'react-use';
import { Environment } from '@kyverno/backstage-plugin-policy-reporter-common';
import {
Environment,
Pagination,
} from '@kyverno/backstage-plugin-policy-reporter-common';
import { useApi } from '@backstage/core-plugin-api';
import { policyReporterApiRef } from '../api';
import {
@ -14,10 +17,15 @@ const DEFAULT_PAGE = 0;
export const usePaginatedPolicies = (
currentEnvironment: Environment,
filter: Filter,
pagination?: Partial<Pagination>,
) => {
const policyReporterApi = useApi(policyReporterApiRef);
const [currentPage, setCurrentPage] = useState<number>(DEFAULT_PAGE);
const [currentOffset, setCurrentOffset] = useState<number>(DEFAULT_OFFSET);
const [currentPage, setCurrentPage] = useState<number>(
pagination?.page ?? DEFAULT_PAGE,
);
const [currentOffset, setCurrentOffset] = useState<number>(
pagination?.offset ?? DEFAULT_OFFSET,
);
const [initialLoading, setInitialLoading] = useState<boolean>(true);
useEffect(() => {
@ -41,7 +49,7 @@ export const usePaginatedPolicies = (
);
setInitialLoading(false);
return data;
}, [currentEnvironment, currentPage, currentOffset]);
}, [currentEnvironment, filter, currentPage, currentOffset]);
return {
policies,

View File

@ -2,4 +2,5 @@ export {
policyReporterPlugin,
EntityKyvernoPoliciesContent,
EntityCustomPoliciesContent,
PolicyReportsPage,
} from './plugin';

View File

@ -43,3 +43,12 @@ export const EntityCustomPoliciesContent = policyReporterPlugin.provide(
mountPoint: rootRouteRef,
}),
);
export const PolicyReportsPage = policyReporterPlugin.provide(
createRoutableExtension({
name: 'PolicyReportsPage',
component: () =>
import('./components/PolicyReportsPage').then(m => m.PolicyReportsPage),
mountPoint: rootRouteRef,
}),
);

View File

@ -1,6 +1,11 @@
{
"extends": "@backstage/cli/config/tsconfig.json",
"include": ["plugins/*/src", "plugins/*/dev", "plugins/*/migrations"],
"include": [
"plugins/*/src",
"plugins/*/dev",
"plugins/*/migrations",
"packages/*/src"
],
"exclude": ["node_modules"],
"compilerOptions": {
"outDir": "dist-types",

7447
yarn.lock

File diff suppressed because it is too large Load Diff