+ );
+};
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:^"