Compare commits
15 Commits
@kyverno/b
...
main
Author | SHA1 | Date |
---|---|---|
|
3390a1d16d | |
|
0410f338dd | |
|
39e9a8bdb1 | |
|
0d6cf4bc8d | |
|
5560993004 | |
|
68cf94e428 | |
|
705c577db3 | |
|
274db4db8c | |
|
6b20c9cfb4 | |
|
4a338477c3 | |
|
00159f48ab | |
|
40e077dac2 | |
|
ac24f2a916 | |
|
727a8b3907 | |
|
1cbf09ac35 |
|
@ -7,5 +7,5 @@
|
|||
"access": "public",
|
||||
"baseBranch": "origin/main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
"ignore": ["app", "backend"]
|
||||
}
|
||||
|
|
|
@ -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.
|
|
@ -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 }}
|
||||
|
|
|
@ -83,3 +83,4 @@ dist-types
|
|||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
/coverage
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"version": "1.38.1"
|
||||
"version": "1.39.1"
|
||||
}
|
||||
|
|
|
@ -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 |
|
@ -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
|
||||
|
||||

|
||||
|
||||
### 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>
|
||||
|
|
15
package.json
15
package.json
|
@ -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",
|
||||
|
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
public
|
|
@ -0,0 +1 @@
|
|||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
</>,
|
||||
);
|
|
@ -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(),
|
||||
];
|
|
@ -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>
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
export { Root } from './Root';
|
|
@ -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>
|
||||
);
|
|
@ -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 />);
|
|
@ -0,0 +1 @@
|
|||
import '@testing-library/jest-dom';
|
|
@ -0,0 +1 @@
|
|||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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();
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export { PolicyReportsDrawerComponent } from './PolicyReportsDrawerComponent';
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export { PolicyReportsPage } from './PolicyReportsPage';
|
|
@ -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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export { SelectSeverity, type SelectSeverityProps } from './SelectSeverity';
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export { SelectStatus, type SelectStatusProps } from './SelectStatus';
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -2,4 +2,5 @@ export {
|
|||
policyReporterPlugin,
|
||||
EntityKyvernoPoliciesContent,
|
||||
EntityCustomPoliciesContent,
|
||||
PolicyReportsPage,
|
||||
} from './plugin';
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue