diff --git a/.changeset/itchy-bulldogs-hunt.md b/.changeset/itchy-bulldogs-hunt.md new file mode 100644 index 0000000000..af03f51f33 --- /dev/null +++ b/.changeset/itchy-bulldogs-hunt.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-puppetdb': minor +--- + +Initial version of [PuppetDB plugin](https://github.com/backstage/backstage/blob/master/plugins/puppetdb/README.md). diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 19e8559e87..3880a0facd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -67,6 +67,7 @@ yarn.lock @backstage/maintainers @back /plugins/newrelic-dashboard @backstage/maintainers @mufaddal7 /plugins/playlist @backstage/maintainers @kuangp /plugins/playlist-* @backstage/maintainers @kuangp +/plugins/puppetdb @backstage/maintainers @tdabasinskas /plugins/rollbar @backstage/maintainers @andrewthauer /plugins/rollbar-backend @backstage/maintainers @andrewthauer /plugins/scaffolder-backend-module-rails @backstage/maintainers @angeliski diff --git a/app-config.yaml b/app-config.yaml index 385c9c2e0e..e2b03786c4 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -123,6 +123,9 @@ proxy: headers: St2-Api-Key: ${ST2_API_KEY} + '/puppetdb': + target: https://your.puppetdb.instance.com + organization: name: My Company diff --git a/microsite/data/plugins/puppetdb.yaml b/microsite/data/plugins/puppetdb.yaml new file mode 100644 index 0000000000..d3b6bd2bcb --- /dev/null +++ b/microsite/data/plugins/puppetdb.yaml @@ -0,0 +1,13 @@ +--- +title: PuppetDB +author: TDabasinskas +authorUrl: https://github.com/tdabasinskas +category: Configuration Management +description: Visualize resource information and Puppet facts from PuppetDB. +documentation: https://github.com/backstage/backstage/blob/master/plugins/puppetdb/README.md +iconUrl: /img/puppet.png +npmPackageName: '@backstage/plugin-puppetdb' +tags: + - puppet + - puppetdb +addedDate: '2023-04-06' diff --git a/microsite/static/img/puppet.png b/microsite/static/img/puppet.png new file mode 100644 index 0000000000..daa3313333 Binary files /dev/null and b/microsite/static/img/puppet.png differ diff --git a/packages/app/package.json b/packages/app/package.json index b9f64ddd75..8eedb5d219 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -51,6 +51,7 @@ "@backstage/plugin-pagerduty": "workspace:^", "@backstage/plugin-permission-react": "workspace:^", "@backstage/plugin-playlist": "workspace:^", + "@backstage/plugin-puppetdb": "workspace:^", "@backstage/plugin-rollbar": "workspace:^", "@backstage/plugin-scaffolder": "workspace:^", "@backstage/plugin-scaffolder-react": "workspace:^", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index ea465e4ed8..19edd9640d 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -108,6 +108,7 @@ import { PlaylistIndexPage } from '@backstage/plugin-playlist'; import { TwoColumnLayout } from './components/scaffolder/customScaffolderLayouts'; import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card'; import { StackstormPage } from '@backstage/plugin-stackstorm'; +import { PuppetDbPage } from '@backstage/plugin-puppetdb'; const app = createApp({ apis, @@ -289,6 +290,7 @@ const routes = ( } /> } /> } /> + } /> ); diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index d07870194b..d2f6fbe109 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -116,6 +116,7 @@ import { EntityRollbarContent, isRollbarAvailable, } from '@backstage/plugin-rollbar'; +import { PuppetDbPage, isPuppetDbAvailable } from '@backstage/plugin-puppetdb'; import { EntitySentryContent } from '@backstage/plugin-sentry'; import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; import { EntityTechInsightsScorecardCard } from '@backstage/plugin-tech-insights'; @@ -836,6 +837,35 @@ const domainPage = ( ); +const resourcePage = ( + + + + {entityWarningContent} + + + + + + + + + + + + + + + + + + +); + export const entityPage = ( @@ -844,6 +874,7 @@ export const entityPage = ( + {defaultEntityPage} diff --git a/plugins/puppetdb/.eslintrc.js b/plugins/puppetdb/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/puppetdb/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/puppetdb/README.md b/plugins/puppetdb/README.md new file mode 100644 index 0000000000..c4fa06b874 --- /dev/null +++ b/plugins/puppetdb/README.md @@ -0,0 +1,63 @@ +# PuppetDB Plugin + +A frontend plugin to integrate PuppetDB with Backstage. When combined with the +[catalog-backend-module-puppetdb](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-puppetdb/README.md) plugin, this +frontend plugin allows viewing PuppetDB reports, including their logs and events, of Backstage resource entities. + +## Getting started + +### Prerequisites + +To get started, you need a running instance of PuppetDB. You can find instructions on how to install it +[here](https://www.puppet.com/docs/puppetdb/7/install_via_module.html). +The PuppetDB [should be configured](https://www.puppet.com/docs/puppetdb/7/configure.html#host) to allow being accessed from your Backstage instance. + +In addition, your Backstage instance need to either have +[catalog-backend-module-puppetdb](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-puppetdb/README.md) plugin installed +or you need to ensure your Resource entities have `puppet.com/certname` annotation set to the PuppetDB node name in some other way. + +### Installation + +1. Install the plugin with `yarn` in the root of your Backstage application directory: + +```bash +yarn --cwd packages/app add @backstage/plugin-puppetdb +``` + +1. Import and use the plugin in `packages/app/src/App.tsx`: + +```tsx +import { PuppetDbPage } from '@backstage/plugin-puppetdb'; + +const routes = ( + + {/* ...other routes */} + } /> + +); +``` + +### Configuration + +1. Configure `puppetdb` proxy. As this plugin uses the Backstage proxy to securely communicate with PuppetDB API, + add the following to your `app-config.yaml` to enable this configuration: + +```yaml +proxy: + '/puppetdb': + target: https://your.puppetdb.instance.com +``` + +## Screenshots + +#### Main page with the reports list: + +![Reports list](https://raw.githubusercontent.com/backstage/backstage/42b65232e763d3e39e2e641b105d2ad469db7a59/plugins/puppetdb/assets/Reports.png) + +#### Events for the specific report: + +![Events](https://raw.githubusercontent.com/backstage/backstage/1b39e86db17f139dc995f02daca4896533e53eb0/plugins/puppetdb/assets/Events.png) + +#### Logs of the specific report: + +![Logs](https://raw.githubusercontent.com/backstage/backstage/1b39e86db17f139dc995f02daca4896533e53eb0/plugins/puppetdb/assets/Logs.png) diff --git a/plugins/puppetdb/api-report.md b/plugins/puppetdb/api-report.md new file mode 100644 index 0000000000..48a3276460 --- /dev/null +++ b/plugins/puppetdb/api-report.md @@ -0,0 +1,35 @@ +## API Report File for "@backstage/plugin-puppetdb" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { PathParams } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; +import { SubRouteRef } from '@backstage/core-plugin-api'; + +// @public +export const isPuppetDbAvailable: (entity: Entity) => boolean; + +// @public +export const PuppetDbPage: () => JSX.Element; + +// @public +const puppetdbPlugin: BackstagePlugin<{}, {}, {}>; +export { puppetdbPlugin as plugin }; +export { puppetdbPlugin }; + +// @public (undocumented) +export const puppetDbReportRouteRef: SubRouteRef>; + +// @public (undocumented) +export const puppetDbRouteRef: RouteRef; + +// @public (undocumented) +export const Router: () => JSX.Element; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/puppetdb/assets/Events.png b/plugins/puppetdb/assets/Events.png new file mode 100644 index 0000000000..163ad67813 Binary files /dev/null and b/plugins/puppetdb/assets/Events.png differ diff --git a/plugins/puppetdb/assets/Logs.png b/plugins/puppetdb/assets/Logs.png new file mode 100644 index 0000000000..f4e7686764 Binary files /dev/null and b/plugins/puppetdb/assets/Logs.png differ diff --git a/plugins/puppetdb/assets/Reports.png b/plugins/puppetdb/assets/Reports.png new file mode 100644 index 0000000000..2bba083fa6 Binary files /dev/null and b/plugins/puppetdb/assets/Reports.png differ diff --git a/plugins/puppetdb/dev/index.tsx b/plugins/puppetdb/dev/index.tsx new file mode 100644 index 0000000000..8082afcfe2 --- /dev/null +++ b/plugins/puppetdb/dev/index.tsx @@ -0,0 +1,19 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createDevApp } from '@backstage/dev-utils'; +import { puppetdbPlugin } from '../src'; + +createDevApp().registerPlugin(puppetdbPlugin).render(); diff --git a/plugins/puppetdb/package.json b/plugins/puppetdb/package.json new file mode 100644 index 0000000000..9c520ff912 --- /dev/null +++ b/plugins/puppetdb/package.json @@ -0,0 +1,68 @@ +{ + "name": "@backstage/plugin-puppetdb", + "description": "Backstage plugin to visualize resource information and Puppet facts from PuppetDB.", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/puppetdb" + }, + "keywords": [ + "backstage", + "puppetdb", + "puppet" + ], + "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", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/catalog-model": "workspace:^", + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.57", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0", + "react-router-dom": "6.0.0-beta.0 || ^6.3.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/node": "*", + "@types/react": "^16.13.1 || ^17.0.0", + "cross-fetch": "^3.1.5", + "msw": "^1.0.1" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/puppetdb/src/api/PupeptDbClient.test.ts b/plugins/puppetdb/src/api/PupeptDbClient.test.ts new file mode 100644 index 0000000000..8b858e21ca --- /dev/null +++ b/plugins/puppetdb/src/api/PupeptDbClient.test.ts @@ -0,0 +1,206 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { UrlPatternDiscovery } from '@backstage/core-app-api'; +import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { PuppetDbClient } from './PuppetDbClient'; +import { + PuppetDbReport, + PuppetDbReportEvent, + PuppetDbReportLog, +} from './types'; + +const server = setupServer(); + +const reports: PuppetDbReport[] = [ + { + hash: 'hash1', + puppet_version: 'version1', + report_format: 1, + certname: 'node1', + start_time: '2021-01-01T00:00:00Z', + end_time: '2021-01-01T00:00:00Z', + producer_timestamp: '2021-01-01T00:00:00Z', + receive_time: '2021-01-01T00:00:00Z', + producer: 'producer1', + transaction_uuid: 'uuid1', + catalog_uuid: 'uuid1', + code_id: 'id1', + cached_catalog_status: 'status1', + type: 'type1', + corrective_change: false, + configuration_version: 'version1', + environment: 'production', + noop: false, + status: 'changed', + }, + { + hash: 'hash2', + puppet_version: 'version2', + report_format: 2, + certname: 'node1', + start_time: '2021-01-01T00:00:00Z', + end_time: '2021-01-01T00:00:00Z', + producer_timestamp: '2021-01-01T00:00:00Z', + receive_time: '2021-01-01T00:00:00Z', + producer: 'producer2', + transaction_uuid: 'uuid2', + catalog_uuid: 'uuid2', + code_id: 'id2', + cached_catalog_status: 'status2', + type: 'type2', + corrective_change: false, + configuration_version: 'version2', + environment: 'production', + noop: false, + status: 'failed', + }, +]; + +const events: PuppetDbReportEvent[] = [ + { + name: 'event1', + message: 'message1', + file: 'file1', + line: 1, + certname: 'node1', + report: 'hash1', + timestamp: '2021-01-01T00:00:00Z', + report_receive_time: '2021-01-01T00:00:00Z', + run_start_time: '2021-01-01T00:00:00Z', + containing_class: 'class1', + environment: 'production', + configuration_version: 'version1', + containment_path: ['path1'], + corrective_change: false, + run_end_time: '2021-01-01T00:00:00Z', + resource_type: 'type1', + resource_title: 'title1', + property: 'property1', + old_value: 'old1', + new_value: 'new1', + status: 'changed', + }, + { + name: 'event2', + message: 'message2', + file: 'file2', + line: 2, + certname: 'node1', + report: 'hash1', + timestamp: '2021-01-01T00:00:00Z', + report_receive_time: '2021-01-01T00:00:00Z', + run_start_time: '2021-01-01T00:00:00Z', + containing_class: 'class2', + environment: 'production', + configuration_version: 'version2', + containment_path: ['path2'], + corrective_change: false, + run_end_time: '2021-01-01T00:00:00Z', + resource_type: 'type2', + resource_title: 'title2', + property: 'property2', + old_value: 'old2', + new_value: 'new2', + status: 'failed', + }, +]; + +const logs: PuppetDbReportLog[] = [ + { + level: 'level1', + file: 'file1', + line: 1, + time: '2021-01-01T00:00:00Z', + message: 'message1', + source: 'source1', + tags: ['tag1'], + }, + { + level: 'level2', + file: 'file2', + line: 2, + time: '2021-01-01T00:00:00Z', + message: 'message2', + source: 'source2', + tags: ['tag2'], + }, +]; + +describe('PuppetDbClient', () => { + setupRequestMockHandlers(server); + + const puppetDbCertName = 'node1'; + const mockBaseUrl = 'http://backstage:9191/api/proxy'; + const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); + let client: PuppetDbClient; + + const setupHandlers = () => { + server.use( + rest.get( + `${mockBaseUrl}/puppetdb/pdb/query/v4/reports`, + (req, res, ctx) => { + if ( + req.url.searchParams.get('query') !== + `["=","certname","${puppetDbCertName}"]` + ) { + return res(ctx.status(400)); + } + + return res(ctx.status(200), ctx.json(reports)); + }, + ), + + rest.get( + `${mockBaseUrl}/puppetdb/pdb/query/v4/reports/hash1/events`, + (_, res, ctx) => { + return res(ctx.status(200), ctx.json(events)); + }, + ), + + rest.get( + `${mockBaseUrl}/puppetdb/pdb/query/v4/reports/hash1/logs`, + (_, res, ctx) => { + return res(ctx.status(200), ctx.json(logs)); + }, + ), + ); + }; + + beforeEach(() => { + setupHandlers(); + client = new PuppetDbClient({ + discoveryApi: discoveryApi, + fetchApi: new MockFetchApi(), + }); + }); + + it('getPuppetDbNodeReports should return reports', async () => { + const got = await client.getPuppetDbNodeReports(puppetDbCertName); + expect(got).toEqual(reports); + }); + + it('getPuppetDbReportEvents should return events', async () => { + const got = await client.getPuppetDbReportEvents('hash1'); + expect(got).toEqual(events); + }); + + it('getPuppetDbReportLogs should return logs', async () => { + const got = await client.getPuppetDbReportLogs('hash1'); + expect(got).toEqual(logs); + }); +}); diff --git a/plugins/puppetdb/src/api/PuppetDbClient.ts b/plugins/puppetdb/src/api/PuppetDbClient.ts new file mode 100644 index 0000000000..b8f3c6e404 --- /dev/null +++ b/plugins/puppetdb/src/api/PuppetDbClient.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + PuppetDbApi, + PuppetDbReport, + PuppetDbReportEvent, + PuppetDbReportLog, +} from './types'; +import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; +import { ResponseError } from '@backstage/errors'; + +export class PuppetDbClient implements PuppetDbApi { + discoveryApi: DiscoveryApi; + fetchApi: FetchApi; + + constructor({ + discoveryApi, + fetchApi, + }: { + discoveryApi: DiscoveryApi; + fetchApi: FetchApi; + }) { + this.discoveryApi = discoveryApi; + this.fetchApi = fetchApi; + } + + private async callApi( + path: string, + query: { [key in string]: any }, + ): Promise { + const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/puppetdb`; + const response = await this.fetchApi.fetch( + `${apiUrl}${path}?${new URLSearchParams(query).toString()}`, + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + if (response.ok) { + return (await response.json()) as T; + } + throw await ResponseError.fromResponse(response); + } + + async getPuppetDbReportEvents( + puppetDbReportHash: string, + ): Promise { + if (!puppetDbReportHash) { + throw new Error('PuppetDB report hash is required'); + } + + const events = (await this.callApi( + `/pdb/query/v4/reports/${puppetDbReportHash}/events`, + {}, + )) as PuppetDbReportEvent[]; + + if (!events || events.length === 0) { + return undefined; + } + + return events; + } + + async getPuppetDbReportLogs( + puppetDbReportHash: string, + ): Promise { + if (!puppetDbReportHash) { + throw new Error('PuppetDB report hash is required'); + } + + const events = (await this.callApi( + `/pdb/query/v4/reports/${puppetDbReportHash}/logs`, + {}, + )) as PuppetDbReportLog[]; + + if (!events || events.length === 0) { + return undefined; + } + + return events; + } + + async getPuppetDbNodeReports( + puppetDbCertName: string, + ): Promise { + if (!puppetDbCertName) { + throw new Error('PuppetDB certname is required'); + } + + return this.callApi(`/pdb/query/v4/reports`, { + query: `["=","certname","${puppetDbCertName}"]`, + order_by: `[{"field": "start_time", "order": "desc"},{"field": "end_time", "order": "desc"}]`, + limit: 100, + }); + } +} diff --git a/plugins/puppetdb/src/api/index.ts b/plugins/puppetdb/src/api/index.ts new file mode 100644 index 0000000000..a21728431d --- /dev/null +++ b/plugins/puppetdb/src/api/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export type { + PuppetDbReport, + PuppetDbReportEvent, + PuppetDbReportLog, +} from './types'; +export { puppetDbApiRef } from './types'; +export { PuppetDbClient } from './PuppetDbClient'; diff --git a/plugins/puppetdb/src/api/types.ts b/plugins/puppetdb/src/api/types.ts new file mode 100644 index 0000000000..8005d0e1de --- /dev/null +++ b/plugins/puppetdb/src/api/types.ts @@ -0,0 +1,198 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createApiRef } from '@backstage/core-plugin-api'; + +/** + * Report from a PuppetDB. + */ +export type PuppetDbReport = { + /** The name of the node that the report was received from. */ + certname: string; + /** The ID of the report. */ + hash: string; + /** The environment assigned to the node that submitted the report. */ + environment: string; + /** The status associated to report's node. */ + status: string; + /** The job id associated with the report. */ + job_id?: string; + /** A flag indicating whether the report was produced by a noop run. */ + noop: boolean; + /** A flag indicating whether the report contains noop events. */ + noop_pending?: boolean; + /** The version of Puppet that generated the report. */ + puppet_version: string; + /** The version number of the report format that Puppet used to generate the original report data. */ + report_format: number; + /** An identifier string that Puppet uses to match a specific catalog for a node to a specific Puppet run. */ + configuration_version: string; + /** The time on the agent at which the Puppet run began. */ + start_time: string; + /** The time on the agent at which the Puppet run ended. */ + end_time: string; + /** The time of catalog submission from the Puppet Server to PuppetDB. */ + producer_timestamp: string; + /** The time at which PuppetDB received the report. */ + receive_time: string; + /** The certname of the Puppet Server that sent the report to PuppetDB. */ + producer: string; + /** A string used to identify a Puppet run. */ + transaction_uuid: string; + /** A string used to tie a catalog to a report to the catalog used from that Puppet run. */ + catalog_uuid: string; + /** A string used to tie a catalog to the Puppet code which generated the catalog. */ + code_id: string; + /** A string used to identify whether the Puppet run used a cached catalogs. */ + cached_catalog_status: string; + /** Either "agent", "plan", or "any" to restrict the results to reports submitted from that source. */ + type: string; + /** A flag indicating whether any of the report's events remediated configuration drift. */ + corrective_change: boolean; + /** Report metrics. */ + metrics?: { + /** Metrics data. */ + data: PuppetDbReportMetric[]; + /** Link to the metrics endpoint. */ + href: string; + }; +}; + +/** + * Resource event generated from Puppet report. + */ +export type PuppetDbReportEvent = { + /** The name of the node on which the event occurred. */ + certname: string; + /** The ID of the report that the event occurred in. */ + report: string; + /** The status of the event. Legal values are success, failure, noop, and skipped. */ + status: string; + /** The timestamp (from the Puppet agent) at which the event occurred. Timestamps are always ISO-8601 compatible date/time strings. */ + timestamp: string; + /** The timestamp (from the Puppet agent) at which the Puppet run began. Timestamps are always ISO-8601 compatible date/time strings. */ + run_start_time: string; + /** The timestamp (from the Puppet agent) at which the Puppet run finished. Timestamps are always ISO-8601 compatible date/time strings. */ + run_end_time: string; + /** The timestamp (from the PuppetDB server) at which the Puppet report was received. Timestamps are always ISO-8601 compatible date/time strings. */ + report_receive_time: string; + /** The type of resource that the event occurred on, such as File, Package, etc. */ + resource_type: string; + /** The title of the resource on which the event occurred. */ + resource_title: string; + /** The property/parameter of the resource on which the event occurred. For example, on a Package resource, this field might have a value of ensure. */ + property?: string; + /** The name of the resource on which the event occurred. */ + name?: string; + /** The new value that Puppet was attempting to set for the specified resource property. Any rich data values will appear as readable strings. */ + new_value?: string; + /** The previous value of the resource property, which Puppet was attempting to change. Any rich data values will appear as readable strings. */ + old_value?: string; + /** A description (supplied by the resource provider) of what happened during the event. */ + message?: string; + /** The manifest file in which the resource definition is located. */ + file?: string; + /** The line (of the containing manifest file) at which the resource definition can be found. */ + line?: number; + /** The Puppet class where this resource is declared. */ + containing_class?: string; + /** Whether the event occurred in the most recent Puppet run (per-node). */ + latest_report?: boolean; + /** The environment associated with the reporting node. */ + environment: string; + /** An identifier string that Puppet uses to match a specific catalog for a node to a specific Puppet run. */ + configuration_version: string; + /** The containment path associated with the event, as an ordered array that ends with the most specific containing element. */ + containment_path: string[]; + /** Whether the event represents a "corrective change", meaning the event rectified configuration drift. */ + corrective_change: boolean; +}; + +/** + * Resource log generated from Puppet report. + */ +export type PuppetDbReportLog = { + /** The manifest file in which the resource definition is located. */ + file?: string; + /** The line (of the containing manifest file) at which the resource definition can be found. */ + line?: number; + /** The timestamp (from the Puppet agent) at which the event occurred. Timestamps are always ISO-8601 compatible date/time strings. */ + time: string; + /** A description (supplied by the resource provider) of what happened during the event. */ + message?: string; + /** The log level. */ + level: string; + /** Log source */ + source: string; + /** Resource tags */ + tags: string[]; +}; + +/** + * A metric from a PuppetDB report. + */ +export type PuppetDbReportMetric = { + /** + * The name of the metric. + */ + name: string; + /** + * The value of the metric. + */ + value: string; + /** + * The category of the metric. + */ + category: string; +}; + +export const puppetDbApiRef = createApiRef({ + id: 'plugin.puppetdb.service', +}); + +/** + * The API provided by the PuppetDB plugin. + */ +export type PuppetDbApi = { + /** + * Get a list of PuppetDB reports for the specified node. + * + * @param puppetDbCertName - The name of the node that the report was received from. + * @returns A list of PuppetDB reports for the specified node. + */ + getPuppetDbNodeReports( + puppetDbCertName: string, + ): Promise; + + /** + * Get a specific PuppetDB report events. + * + * @param puppetDbReportHash - The ID of the report. + * @returns Events from a specific PuppetDB report. + */ + getPuppetDbReportEvents( + puppetDbReportHash: string, + ): Promise; + + /** + * Get a specific PuppetDB report logs. + * + * @param puppetDbReportHash - The ID of the report. + * @returns Logs from a specific PuppetDB report. + */ + getPuppetDbReportLogs( + puppetDbReportHash: string, + ): Promise; +}; diff --git a/plugins/puppetdb/src/components/ReportDetailsPage/ReportDetailsEventsTable.test.tsx b/plugins/puppetdb/src/components/ReportDetailsPage/ReportDetailsEventsTable.test.tsx new file mode 100644 index 0000000000..31775014f2 --- /dev/null +++ b/plugins/puppetdb/src/components/ReportDetailsPage/ReportDetailsEventsTable.test.tsx @@ -0,0 +1,105 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ReportDetailsEventsTable } from './ReportDetailsEventsTable'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; +import { puppetDbApiRef, PuppetDbReportEvent } from '../../api'; +import { PuppetDbClient } from '../../api'; +import { ApiProvider } from '@backstage/core-app-api'; +import { puppetDbRouteRef } from '../../routes'; + +const events: PuppetDbReportEvent[] = [ + { + name: 'event3', + message: 'message1', + file: 'file1', + line: 1, + certname: 'node1', + report: 'hash1', + timestamp: '2021-01-01T00:00:00Z', + report_receive_time: '2021-01-01T00:00:00Z', + run_start_time: '2021-01-01T00:00:00Z', + containing_class: 'class1', + environment: 'production', + configuration_version: 'version1', + containment_path: ['path1'], + corrective_change: false, + run_end_time: '2021-01-01T00:00:00Z', + resource_type: 'type1', + resource_title: 'title1', + property: 'property1', + old_value: 'old1', + new_value: 'new1', + status: 'changed', + }, + { + name: 'event2', + message: 'message2', + file: 'file2', + line: 2, + certname: 'node1', + report: 'hash1', + timestamp: '2021-01-01T00:00:00Z', + report_receive_time: '2021-01-01T00:00:00Z', + run_start_time: '2021-01-01T00:00:00Z', + containing_class: 'class2', + environment: 'production', + configuration_version: 'version2', + containment_path: ['path2'], + corrective_change: false, + run_end_time: '2021-01-01T00:00:00Z', + resource_type: 'type2', + resource_title: 'title2', + property: 'property2', + old_value: 'old2', + new_value: 'new2', + status: 'failed', + }, +]; + +const mockPuppetDbApi: Partial = { + getPuppetDbReportEvents: async () => events, +}; + +const apis = TestApiRegistry.from([puppetDbApiRef, mockPuppetDbApi]); + +const hash = 'hash'; + +describe('ReportEventsTable', () => { + it('should render', async () => { + const render = await renderInTestApp( + + + , + { + mountedRoutes: { + '/puppetdb/report/:hash': puppetDbRouteRef, + }, + }, + ); + + expect(render.getByText('Latest events')).toBeInTheDocument(); + expect(render.getByText('Containing Class')).toBeInTheDocument(); + expect(render.getByText('Resource')).toBeInTheDocument(); + expect(render.getByText('Property')).toBeInTheDocument(); + expect(render.getByText('Old Value')).toBeInTheDocument(); + expect(render.getByText('New Value')).toBeInTheDocument(); + expect(render.getByText('Status')).toBeInTheDocument(); + expect(render.getByText('property2')).toBeInTheDocument(); + expect(render.getByText('old1')).toBeInTheDocument(); + expect(render.getByText('old2')).toBeInTheDocument(); + }); +}); diff --git a/plugins/puppetdb/src/components/ReportDetailsPage/ReportDetailsEventsTable.tsx b/plugins/puppetdb/src/components/ReportDetailsPage/ReportDetailsEventsTable.tsx new file mode 100644 index 0000000000..b7f5b3c436 --- /dev/null +++ b/plugins/puppetdb/src/components/ReportDetailsPage/ReportDetailsEventsTable.tsx @@ -0,0 +1,145 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Typography from '@material-ui/core/Typography'; +import React from 'react'; +import { puppetDbApiRef, PuppetDbReportEvent } from '../../api'; +import { + ResponseErrorPanel, + Table, + TableColumn, +} from '@backstage/core-components'; +import useAsync from 'react-use/lib/useAsync'; +import { useApi } from '@backstage/core-plugin-api'; +import { makeStyles } from '@material-ui/core/styles'; +import { StatusField } from '../StatusField'; + +type ReportEventsTableProps = { + hash: string; +}; + +const useStyles = makeStyles(theme => ({ + empty: { + padding: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + }, +})); + +/** + * Component for displaying PuppetDB report events. + * + * @public + */ +export const ReportDetailsEventsTable = (props: ReportEventsTableProps) => { + const { hash } = props; + const puppetDbApi = useApi(puppetDbApiRef); + const classes = useStyles(); + const { value, loading, error } = useAsync(async () => { + return puppetDbApi.getPuppetDbReportEvents(hash); + }, [puppetDbApi, hash]); + + if (error) { + return ; + } + + const columns: TableColumn[] = [ + { + title: 'Run Start Time', + field: 'run_start_time', + align: 'center', + width: '300px', + render: rowData => ( + + {new Date(Date.parse(rowData.run_start_time)).toLocaleString()} + + ), + }, + { + title: 'Run End Time', + field: 'run_end_time', + align: 'center', + width: '300px', + render: rowData => ( + + {new Date(Date.parse(rowData.run_end_time)).toLocaleString()} + + ), + }, + { + title: 'Containing Class', + field: 'containing_class', + render: rowData => ( + + {rowData.containing_class} + + ), + }, + { + title: 'Resource', + field: 'resource_title', + render: rowData => ( + + {rowData.resource_type}[{rowData.resource_title}] + + ), + }, + { + title: 'Property', + field: 'property', + render: rowData => {rowData.property}, + }, + { + title: 'Old Value', + field: 'old_value', + render: rowData => {rowData.old_value}, + }, + { + title: 'New Value', + field: 'new_value', + render: rowData => {rowData.new_value}, + }, + { + title: 'Status', + field: 'status', + render: rowData => , + }, + ]; + + return ( + + No events + + } + title="Latest events" + columns={columns} + data={value || []} + isLoading={loading} + /> + ); +}; diff --git a/plugins/puppetdb/src/components/ReportDetailsPage/ReportDetailsLogsTable.test.tsx b/plugins/puppetdb/src/components/ReportDetailsPage/ReportDetailsLogsTable.test.tsx new file mode 100644 index 0000000000..2d9d973412 --- /dev/null +++ b/plugins/puppetdb/src/components/ReportDetailsPage/ReportDetailsLogsTable.test.tsx @@ -0,0 +1,74 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ReportDetailsLogsTable } from './ReportDetailsLogsTable'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; +import { puppetDbApiRef, PuppetDbReportLog } from '../../api'; +import { PuppetDbClient } from '../../api'; +import { ApiProvider } from '@backstage/core-app-api'; +import { puppetDbRouteRef } from '../../routes'; + +const logs: PuppetDbReportLog[] = [ + { + level: 'level1', + file: 'file1', + line: 1, + time: '2021-01-01T00:00:00Z', + message: 'message1', + source: 'source1', + tags: ['tag1'], + }, + { + level: 'level2', + file: 'file2', + line: 2, + time: '2021-01-01T00:00:00Z', + message: 'message2', + source: 'source2', + tags: ['tag2'], + }, +]; + +const mockPuppetDbApi: Partial = { + getPuppetDbReportLogs: async () => logs, +}; + +const apis = TestApiRegistry.from([puppetDbApiRef, mockPuppetDbApi]); + +const hash = 'hash'; + +describe('ReportLogsTable', () => { + it('should render', async () => { + const render = await renderInTestApp( + + + , + { + mountedRoutes: { + '/puppetdb/report/:hash': puppetDbRouteRef, + }, + }, + ); + + expect(render.getByText('Latest logs')).toBeInTheDocument(); + expect(render.getByText('Level')).toBeInTheDocument(); + expect(render.getByText('Timestamp')).toBeInTheDocument(); + expect(render.getByText('Source')).toBeInTheDocument(); + expect(render.getByText('Message')).toBeInTheDocument(); + expect(render.getByText('source1')).toBeInTheDocument(); + expect(render.getByText('source2')).toBeInTheDocument(); + }); +}); diff --git a/plugins/puppetdb/src/components/ReportDetailsPage/ReportDetailsLogsTable.tsx b/plugins/puppetdb/src/components/ReportDetailsPage/ReportDetailsLogsTable.tsx new file mode 100644 index 0000000000..f512fcd935 --- /dev/null +++ b/plugins/puppetdb/src/components/ReportDetailsPage/ReportDetailsLogsTable.tsx @@ -0,0 +1,133 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Typography from '@material-ui/core/Typography'; +import React from 'react'; +import { puppetDbApiRef, PuppetDbReportLog } from '../../api'; +import { + ResponseErrorPanel, + Table, + TableColumn, +} from '@backstage/core-components'; +import useAsync from 'react-use/lib/useAsync'; +import { useApi } from '@backstage/core-plugin-api'; +import { makeStyles } from '@material-ui/core/styles'; +import { BackstageTheme } from '@backstage/theme'; + +type ReportLogsTableProps = { + hash: string; +}; + +const useStyles = makeStyles(theme => ({ + empty: { + padding: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + }, + level_error: { + color: theme.palette.error.light, + }, + level_warning: { + color: theme.palette.warning.light, + }, + level_notice: { + color: theme.palette.info.light, + }, +})); + +/** + * Component for displaying PuppetDB report logs. + * + * @public + */ +export const ReportDetailsLogsTable = (props: ReportLogsTableProps) => { + const { hash } = props; + const puppetDbApi = useApi(puppetDbApiRef); + const classes = useStyles(); + const { value, loading, error } = useAsync(async () => { + return puppetDbApi.getPuppetDbReportLogs(hash); + }, [puppetDbApi, hash]); + + if (error) { + return ; + } + + const columns: TableColumn[] = [ + { + title: 'Level', + field: 'level', + align: 'center', + width: '100px', + render: rowData => ( + + {rowData.level.toLocaleUpperCase('en-US')} + + ), + }, + { + title: 'Timestamp', + field: 'time', + align: 'center', + width: '300px', + render: rowData => ( + + {new Date(Date.parse(rowData.time)).toLocaleString()} + + ), + }, + { + title: 'Source', + field: 'source', + render: rowData => {rowData.source}, + }, + { + title: 'Message', + field: 'message', + render: rowData => {rowData.message}, + }, + ]; + + return ( +
+ No logs + + } + title="Latest logs" + columns={columns} + data={value || []} + isLoading={loading} + /> + ); +}; diff --git a/plugins/puppetdb/src/components/ReportDetailsPage/ReportDetailsPage.test.tsx b/plugins/puppetdb/src/components/ReportDetailsPage/ReportDetailsPage.test.tsx new file mode 100644 index 0000000000..e6ade0e596 --- /dev/null +++ b/plugins/puppetdb/src/components/ReportDetailsPage/ReportDetailsPage.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ReportDetailsPage } from './ReportDetailsPage'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; +import { puppetDbRouteRef } from '../../routes'; +import { puppetDbApiRef, PuppetDbClient } from '../../api'; +import { ApiProvider } from '@backstage/core-app-api'; + +const mockPuppetDbApi: Partial = { + getPuppetDbReportEvents: async () => [], +}; + +const apis = TestApiRegistry.from([puppetDbApiRef, mockPuppetDbApi]); + +describe('ReportDetailsPage', () => { + it('should render', async () => { + const render = await renderInTestApp( + + + , + { + mountedRoutes: { + '/puppetdb/report': puppetDbRouteRef, + }, + }, + ); + expect(render.getByText('PuppetDB Reports')).toBeInTheDocument(); + expect(render.getByText('Events')).toBeInTheDocument(); + expect(render.getByText('Logs')).toBeInTheDocument(); + }); +}); diff --git a/plugins/puppetdb/src/components/ReportDetailsPage/ReportDetailsPage.tsx b/plugins/puppetdb/src/components/ReportDetailsPage/ReportDetailsPage.tsx new file mode 100644 index 0000000000..db6536fce4 --- /dev/null +++ b/plugins/puppetdb/src/components/ReportDetailsPage/ReportDetailsPage.tsx @@ -0,0 +1,106 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Link as RouterLink, useParams } from 'react-router-dom'; +import { Breadcrumbs, Link } from '@backstage/core-components'; +import { makeStyles } from '@material-ui/core/styles'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import { puppetDbRouteRef } from '../../routes'; +import React, { useState } from 'react'; +import { + Card, + CardContent, + Tab, + Box, + Typography, + Tabs, +} from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; +import { ReportDetailsEventsTable } from './ReportDetailsEventsTable'; +import { ReportDetailsLogsTable } from './ReportDetailsLogsTable'; + +const useStyles = makeStyles(theme => ({ + cards: { + marginTop: theme.spacing(2), + }, + tabs: { + borderBottom: `1px solid ${theme.palette.textVerySubtle}`, + backgroundColor: theme.palette.background.default, + padding: theme.spacing(0, 4), + }, + default: { + padding: theme.spacing(2), + fontWeight: theme.typography.fontWeightBold, + color: theme.palette.text.secondary, + textTransform: 'uppercase', + }, + selected: { + color: theme.palette.text.primary, + }, +})); + +/** + * Component for displaying the details of a PuppetDB report. + * + * @public + */ +export const ReportDetailsPage = () => { + const { hash = '' } = useParams(); + const classes = useStyles(); + const [tabIndex, setTabIndex] = useState(0); + const reportsRouteLink = useRouteRef(puppetDbRouteRef); + const tabs = [ + { id: 'events', label: 'Events' }, + { id: 'logs', label: 'Logs' }, + ]; + const safeTabIndex = tabIndex > tabs.length - 1 ? 0 : tabIndex; + + return ( +
+ + + PuppetDB Reports + + {hash} + + + + setTabIndex(index)} + value={safeTabIndex} + > + {tabs.map((tab, index) => ( + + ))} + + + {safeTabIndex === 0 && } + {safeTabIndex === 1 && } + + + +
+ ); +}; diff --git a/plugins/puppetdb/src/components/ReportDetailsPage/index.ts b/plugins/puppetdb/src/components/ReportDetailsPage/index.ts new file mode 100644 index 0000000000..8c0153c853 --- /dev/null +++ b/plugins/puppetdb/src/components/ReportDetailsPage/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { ReportDetailsPage } from './ReportDetailsPage'; +export { ReportDetailsEventsTable } from './ReportDetailsEventsTable'; +export { ReportDetailsLogsTable } from './ReportDetailsLogsTable'; diff --git a/plugins/puppetdb/src/components/ReportsPage/ReportsPage.test.tsx b/plugins/puppetdb/src/components/ReportsPage/ReportsPage.test.tsx new file mode 100644 index 0000000000..a4c094f69a --- /dev/null +++ b/plugins/puppetdb/src/components/ReportsPage/ReportsPage.test.tsx @@ -0,0 +1,85 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ReportsPage } from './ReportsPage'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { puppetDbApiRef, PuppetDbClient } from '../../api'; +import { ANNOTATION_PUPPET_CERTNAME } from '../../constants'; +import { Entity } from '@backstage/catalog-model/'; +import { isPuppetDbAvailable } from '../Router'; +import { ApiProvider } from '@backstage/core-app-api'; +import { puppetDbRouteRef } from '../../routes'; + +const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Resource', + metadata: { + name: 'test', + annotations: { + [ANNOTATION_PUPPET_CERTNAME]: 'node1', + }, + }, +}; + +const entityWithoutAnnotations: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Resource', + metadata: { + name: 'test', + annotations: {}, + }, +}; + +const mockPuppetDbApi: Partial = { + getPuppetDbNodeReports: async () => [], +}; + +const apis = TestApiRegistry.from([puppetDbApiRef, mockPuppetDbApi]); + +describe('isPuppetDbAvailable', () => { + describe('when entity has no annotations', () => { + it('returns false', () => { + expect(isPuppetDbAvailable(entityWithoutAnnotations)).toBe(false); + }); + }); + + describe('when entity has the puppet annotation', () => { + it('returns true', () => { + expect(isPuppetDbAvailable(entity)).toBe(true); + }); + }); +}); + +describe('ReportsPage', () => { + it('should render', async () => { + const render = await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/puppetdb': puppetDbRouteRef, + }, + }, + ); + expect( + render.getByText('Latest PuppetDB reports from node node1'), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/puppetdb/src/components/ReportsPage/ReportsPage.tsx b/plugins/puppetdb/src/components/ReportsPage/ReportsPage.tsx new file mode 100644 index 0000000000..8c99e590b5 --- /dev/null +++ b/plugins/puppetdb/src/components/ReportsPage/ReportsPage.tsx @@ -0,0 +1,39 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ReportsTable } from './ReportsTable'; +import { Page, Content } from '@backstage/core-components'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { ANNOTATION_PUPPET_CERTNAME } from '../../constants'; + +/** + * Component to display PuppetDB reports page. + * + * @public + */ +export const ReportsPage = () => { + const { entity } = useEntity(); + const certName = + entity?.metadata?.annotations?.[ANNOTATION_PUPPET_CERTNAME] ?? ''; + + return ( + + + + + + ); +}; diff --git a/plugins/puppetdb/src/components/ReportsPage/ReportsTable.test.tsx b/plugins/puppetdb/src/components/ReportsPage/ReportsTable.test.tsx new file mode 100644 index 0000000000..fadfb1a3f9 --- /dev/null +++ b/plugins/puppetdb/src/components/ReportsPage/ReportsTable.test.tsx @@ -0,0 +1,103 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ReportsTable } from './ReportsTable'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; +import { puppetDbApiRef, PuppetDbReport, PuppetDbClient } from '../../api'; +import { ApiProvider } from '@backstage/core-app-api'; +import { puppetDbRouteRef } from '../../routes'; + +const reports: PuppetDbReport[] = [ + { + hash: 'hash1', + puppet_version: 'version1', + report_format: 1, + certname: 'node1', + start_time: '2021-01-01T00:00:00Z', + end_time: '2021-01-01T00:00:00Z', + producer_timestamp: '2021-01-01T00:00:00Z', + receive_time: '2021-01-01T00:00:00Z', + producer: 'producer1', + transaction_uuid: 'uuid1', + catalog_uuid: 'uuid1', + code_id: 'id1', + cached_catalog_status: 'status1', + type: 'type1', + corrective_change: false, + configuration_version: 'version1', + environment: 'production', + noop: true, + status: 'changed', + }, + { + hash: 'hash2', + puppet_version: 'version2', + report_format: 2, + certname: 'node1', + start_time: '2021-01-01T00:00:00Z', + end_time: '2021-01-01T00:00:00Z', + producer_timestamp: '2021-01-01T00:00:00Z', + receive_time: '2021-01-01T00:00:00Z', + producer: 'producer2', + transaction_uuid: 'uuid2', + catalog_uuid: 'uuid2', + code_id: 'id2', + cached_catalog_status: 'status2', + type: 'type2', + corrective_change: false, + configuration_version: 'version2', + environment: 'production', + noop: false, + status: 'failed', + }, +]; + +const mockPuppetDbApi: Partial = { + getPuppetDbNodeReports: async () => reports, +}; + +const apis = TestApiRegistry.from([puppetDbApiRef, mockPuppetDbApi]); + +const certName = 'node1'; + +describe('ReportsTable', () => { + it('should render', async () => { + const render = await renderInTestApp( + + + , + { + mountedRoutes: { + '/puppetdb': puppetDbRouteRef, + }, + }, + ); + + expect( + render.getByText('Latest PuppetDB reports from node node1'), + ).toBeInTheDocument(); + expect(render.getByText('Configuration Version')).toBeInTheDocument(); + expect(render.getByText('Start Time')).toBeInTheDocument(); + expect(render.getByText('End Time')).toBeInTheDocument(); + expect(render.getByText('Run Duration')).toBeInTheDocument(); + expect(render.getByText('Mode')).toBeInTheDocument(); + expect(render.getByText('Status')).toBeInTheDocument(); + expect(render.getByText('NO-NOOP')).toBeInTheDocument(); + expect(render.getByText('NOOP')).toBeInTheDocument(); + expect(render.getByText('FAILED')).toBeInTheDocument(); + expect(render.getByText('CHANGED')).toBeInTheDocument(); + }); +}); diff --git a/plugins/puppetdb/src/components/ReportsPage/ReportsTable.tsx b/plugins/puppetdb/src/components/ReportsPage/ReportsTable.tsx new file mode 100644 index 0000000000..3aeecc70b0 --- /dev/null +++ b/plugins/puppetdb/src/components/ReportsPage/ReportsTable.tsx @@ -0,0 +1,169 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { puppetDbApiRef, PuppetDbReport } from '../../api'; +import useAsync from 'react-use/lib/useAsync'; +import React from 'react'; +import { + Link, + ResponseErrorPanel, + Table, + TableColumn, +} from '@backstage/core-components'; +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { makeStyles } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; +import { Link as RouterLink } from 'react-router-dom'; +import { puppetDbReportRouteRef } from '../../routes'; +import { StatusField } from '../StatusField'; + +type ReportsTableProps = { + certName: string; +}; + +const useStyles = makeStyles(theme => ({ + empty: { + padding: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + }, +})); + +/** + * Component for displaying a table of PuppetDB reports for a given node. + * + * @public + */ +export const ReportsTable = (props: ReportsTableProps) => { + const { certName } = props; + const puppetDbApi = useApi(puppetDbApiRef); + const reportsRouteLink = useRouteRef(puppetDbReportRouteRef); + const classes = useStyles(); + + const { value, loading, error } = useAsync(async () => { + return puppetDbApi.getPuppetDbNodeReports(certName); + }, [puppetDbApi, certName]); + + if (error) { + return ; + } + + const columns: TableColumn[] = [ + { + title: 'Configuration Version', + field: 'configuration_version', + render: rowData => ( + + {rowData.configuration_version !== '' ? ( + {rowData.configuration_version} + ) : ( + + (N/A) + + )} + + ), + }, + { + title: 'Start Time', + field: 'start_time', + align: 'center', + width: '300px', + render: rowData => ( + + {new Date(Date.parse(rowData.start_time)).toLocaleString()} + + ), + }, + { + title: 'End Time', + field: 'end_time', + align: 'center', + width: '300px', + render: rowData => ( + + {new Date(Date.parse(rowData.end_time)).toLocaleString()} + + ), + }, + { + title: 'Run Duration', + align: 'center', + width: '400px', + render: rowData => { + const start_date = new Date(Date.parse(rowData.start_time)); + const end_date = new Date(Date.parse(rowData.end_time)); + const duration = new Date(end_date.getTime() - start_date.getTime()); + return ( + + {duration.getUTCHours().toString().padStart(2, '0')}: + {duration.getUTCMinutes().toString().padStart(2, '0')}: + {duration.getUTCSeconds().toString().padStart(2, '0')}. + {duration.getUTCMilliseconds().toString().padStart(4, '0')} + + ); + }, + }, + { + title: 'Environment', + field: 'environment', + }, + { + title: 'Mode', + field: 'noop', + align: 'center', + render: rowData => + rowData.noop ? ( + NOOP + ) : ( + NO-NOOP + ), + }, + { + title: 'Status', + field: 'status', + align: 'center', + render: rowData => , + }, + ]; + + return ( +
+ No reports + + } + title={`Latest PuppetDB reports from node ${certName}`} + columns={columns} + data={value || []} + isLoading={loading} + /> + ); +}; diff --git a/plugins/puppetdb/src/components/ReportsPage/index.ts b/plugins/puppetdb/src/components/ReportsPage/index.ts new file mode 100644 index 0000000000..60840e5eb5 --- /dev/null +++ b/plugins/puppetdb/src/components/ReportsPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { ReportsPage } from './ReportsPage'; +export { ReportsTable } from './ReportsTable'; diff --git a/plugins/puppetdb/src/components/Router.tsx b/plugins/puppetdb/src/components/Router.tsx new file mode 100644 index 0000000000..3a8a05911b --- /dev/null +++ b/plugins/puppetdb/src/components/Router.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Routes, Route } from 'react-router-dom'; +import { puppetDbReportRouteRef } from '../routes'; +import { ANNOTATION_PUPPET_CERTNAME } from '../constants'; +import { Entity } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { MissingAnnotationEmptyState } from '@backstage/core-components'; +import { ReportsPage } from './ReportsPage'; +import { ReportDetailsPage } from './ReportDetailsPage'; + +/** + * Checks if the entity has a puppet certname annotation. + * @param entity - The entity to check for the puppet certname annotation. + * + * @public + */ +export const isPuppetDbAvailable = (entity: Entity) => + Boolean(entity.metadata.annotations?.[ANNOTATION_PUPPET_CERTNAME]); + +/** @public */ +export const Router = () => { + const { entity } = useEntity(); + + if (!isPuppetDbAvailable(entity)) { + return ( + + ); + } + + return ( + + } /> + } + /> + + ); +}; diff --git a/plugins/puppetdb/src/components/StatusField/StatusField.test.tsx b/plugins/puppetdb/src/components/StatusField/StatusField.test.tsx new file mode 100644 index 0000000000..1aa8c94ce9 --- /dev/null +++ b/plugins/puppetdb/src/components/StatusField/StatusField.test.tsx @@ -0,0 +1,52 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { StatusField } from './StatusField'; +import { renderInTestApp } from '@backstage/test-utils'; + +describe('StatusField', () => { + it('should render failed', async () => { + const render = await renderInTestApp(); + expect(render.getByText('FAILED')).toBeInTheDocument(); + expect( + render.container.querySelector('span[aria-label="Status error"]'), + ).toBeInTheDocument(); + }); + + it('should render changed', async () => { + const render = await renderInTestApp(); + expect(render.getByText('CHANGED')).toBeInTheDocument(); + expect( + render.container.querySelector('span[aria-label="Status running"]'), + ).toBeInTheDocument(); + }); + + it('should render unchanged', async () => { + const render = await renderInTestApp(); + expect(render.getByText('UNCHANGED')).toBeInTheDocument(); + expect( + render.container.querySelector('span[aria-label="Status pending"]'), + ).toBeInTheDocument(); + }); + + it('should render default', async () => { + const render = await renderInTestApp(); + expect(render.getByText('SUCCESSFUL')).toBeInTheDocument(); + expect( + render.container.querySelector('span[aria-label="Status ok"]'), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/puppetdb/src/components/StatusField/StatusField.tsx b/plugins/puppetdb/src/components/StatusField/StatusField.tsx new file mode 100644 index 0000000000..7c1d1b1efc --- /dev/null +++ b/plugins/puppetdb/src/components/StatusField/StatusField.tsx @@ -0,0 +1,68 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { + StatusPending, + StatusRunning, + StatusOK, + StatusError, +} from '@backstage/core-components'; + +type StatusCellProps = { + status: string; +}; + +/** + * Component to display status field for Puppet reports and events. + * + * @public + */ +export const StatusField = (props: StatusCellProps) => { + const { status } = props; + + const statusUC = status.toLocaleUpperCase('en-US'); + switch (status) { + case 'failed': + return ( + <> + + {statusUC} + + ); + case 'changed': + return ( + <> + + {statusUC} + + ); + case 'unchanged': + return ( + <> + + {statusUC} + + ); + default: + return ( + <> + + {statusUC} + + ); + } +}; diff --git a/plugins/puppetdb/src/components/StatusField/index.ts b/plugins/puppetdb/src/components/StatusField/index.ts new file mode 100644 index 0000000000..4f1a8aad1c --- /dev/null +++ b/plugins/puppetdb/src/components/StatusField/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { StatusField } from './StatusField'; diff --git a/plugins/puppetdb/src/constants.ts b/plugins/puppetdb/src/constants.ts new file mode 100644 index 0000000000..57fb4373f4 --- /dev/null +++ b/plugins/puppetdb/src/constants.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Annotation for specifying the certificate name of a node in PuppetDB. + * + * @public + */ +export const ANNOTATION_PUPPET_CERTNAME = 'puppet.com/certname'; diff --git a/plugins/puppetdb/src/index.ts b/plugins/puppetdb/src/index.ts new file mode 100644 index 0000000000..2361a44bed --- /dev/null +++ b/plugins/puppetdb/src/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { + puppetdbPlugin, + puppetdbPlugin as plugin, + PuppetDbPage, +} from './plugin'; +export { Router, isPuppetDbAvailable } from './components/Router'; +export * from './routes'; diff --git a/plugins/puppetdb/src/plugin.test.ts b/plugins/puppetdb/src/plugin.test.ts new file mode 100644 index 0000000000..947b88d129 --- /dev/null +++ b/plugins/puppetdb/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { puppetdbPlugin } from './plugin'; + +describe('puppetdb', () => { + it('should export plugin', () => { + expect(puppetdbPlugin).toBeDefined(); + }); +}); diff --git a/plugins/puppetdb/src/plugin.ts b/plugins/puppetdb/src/plugin.ts new file mode 100644 index 0000000000..91a2cbf6c6 --- /dev/null +++ b/plugins/puppetdb/src/plugin.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + createApiFactory, + createPlugin, + createRoutableExtension, + discoveryApiRef, + fetchApiRef, +} from '@backstage/core-plugin-api'; + +import { puppetDbApiRef, PuppetDbClient } from './api'; +import { puppetDbRouteRef } from './routes'; + +/** + * Create the PuppetDB frontend plugin. + * + * @public + * */ +export const puppetdbPlugin = createPlugin({ + id: 'puppetDb', + apis: [ + createApiFactory({ + api: puppetDbApiRef, + deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef }, + factory: ({ discoveryApi, fetchApi }) => + new PuppetDbClient({ discoveryApi, fetchApi }), + }), + ], +}); + +/** + * Creates a routable extension for the PuppetDB plugin content. + * + * @public + */ +export const PuppetDbPage = puppetdbPlugin.provide( + createRoutableExtension({ + name: 'PuppetDbPage', + component: () => import('./components/Router').then(m => m.Router), + mountPoint: puppetDbRouteRef, + }), +); diff --git a/plugins/puppetdb/src/routes.ts b/plugins/puppetdb/src/routes.ts new file mode 100644 index 0000000000..d6812f9f2c --- /dev/null +++ b/plugins/puppetdb/src/routes.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createRouteRef, createSubRouteRef } from '@backstage/core-plugin-api'; + +/** @public */ +export const puppetDbRouteRef = createRouteRef({ + id: 'puppetdb', +}); + +/** @public */ +export const puppetDbReportRouteRef = createSubRouteRef({ + id: 'puppetdb/report', + path: '/:hash', + parent: puppetDbRouteRef, +}); diff --git a/plugins/puppetdb/src/setupTests.ts b/plugins/puppetdb/src/setupTests.ts new file mode 100644 index 0000000000..73dd8dce47 --- /dev/null +++ b/plugins/puppetdb/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/yarn.lock b/yarn.lock index b8327d2481..d605554ed7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7924,6 +7924,37 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-puppetdb@workspace:^, @backstage/plugin-puppetdb@workspace:plugins/puppetdb": + version: 0.0.0-use.local + resolution: "@backstage/plugin-puppetdb@workspace:plugins/puppetdb" + dependencies: + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": ^4.0.0-alpha.57 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + "@types/node": "*" + "@types/react": ^16.13.1 || ^17.0.0 + cross-fetch: ^3.1.5 + msw: ^1.0.1 + react-use: ^17.2.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + languageName: unknown + linkType: soft + "@backstage/plugin-rollbar-backend@workspace:^, @backstage/plugin-rollbar-backend@workspace:plugins/rollbar-backend": version: 0.0.0-use.local resolution: "@backstage/plugin-rollbar-backend@workspace:plugins/rollbar-backend" @@ -23630,6 +23661,7 @@ __metadata: "@backstage/plugin-pagerduty": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" "@backstage/plugin-playlist": "workspace:^" + "@backstage/plugin-puppetdb": "workspace:^" "@backstage/plugin-rollbar": "workspace:^" "@backstage/plugin-scaffolder": "workspace:^" "@backstage/plugin-scaffolder-react": "workspace:^"