diff --git a/.changeset/grumpy-carrots-sin.md b/.changeset/grumpy-carrots-sin.md
new file mode 100644
index 0000000000..8f2843ebd0
--- /dev/null
+++ b/.changeset/grumpy-carrots-sin.md
@@ -0,0 +1,6 @@
+---
+'@backstage/plugin-catalog-unprocessed-entities': minor
+'@backstage/plugin-catalog-backend-module-unprocessed': minor
+---
+
+Added a new plugin to expose entities which are unprocessed or have errors processing
diff --git a/packages/app/package.json b/packages/app/package.json
index b7c75009fe..dd13ae8f1d 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -26,6 +26,7 @@
"@backstage/plugin-catalog-graph": "workspace:^",
"@backstage/plugin-catalog-import": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
+ "@backstage/plugin-catalog-unprocessed-entities": "workspace:^",
"@backstage/plugin-circleci": "workspace:^",
"@backstage/plugin-cloudbuild": "workspace:^",
"@backstage/plugin-code-coverage": "workspace:^",
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index d46774a757..df54f37dc6 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -111,6 +111,7 @@ import { StackstormPage } from '@backstage/plugin-stackstorm';
import { PuppetDbPage } from '@backstage/plugin-puppetdb';
import { DevToolsPage } from '@backstage/plugin-devtools';
import { customDevToolsPage } from './components/devtools/CustomDevToolsPage';
+import { CatalogUnprocessedEntitiesPage } from '@backstage/plugin-catalog-unprocessed-entities';
const app = createApp({
apis,
@@ -171,6 +172,10 @@ const routes = (
>
{entityPage}
+ }
+ />
Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { BackendFeature } from '@backstage/backend-plugin-api';
+import { HttpRouterService } from '@backstage/backend-plugin-api';
+import { Knex } from 'knex';
+
+// @public
+export const catalogModuleUnprocessedEntities: () => BackendFeature;
+
+// @public
+export class UnprocessedEntitesModule {
+ constructor(database: Knex, router: HttpRouterService);
+ // (undocumented)
+ registerRoutes(): void;
+}
+```
diff --git a/plugins/catalog-backend-module-unprocessed/package.json b/plugins/catalog-backend-module-unprocessed/package.json
new file mode 100644
index 0000000000..55228d5ab5
--- /dev/null
+++ b/plugins/catalog-backend-module-unprocessed/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "@backstage/plugin-catalog-backend-module-unprocessed",
+ "description": "Backstage Catalog module to view unprocessed entities",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "backend-plugin-module"
+ },
+ "scripts": {
+ "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"
+ },
+ "devDependencies": {
+ "@backstage/cli": "workspace:^"
+ },
+ "files": [
+ "dist"
+ ],
+ "dependencies": {
+ "@backstage/backend-plugin-api": "workspace:^",
+ "@backstage/catalog-model": "workspace:^",
+ "@backstage/plugin-auth-node": "workspace:^",
+ "express-promise-router": "^4.1.1",
+ "knex": "^2.4.2"
+ }
+}
diff --git a/plugins/catalog-backend-module-unprocessed/src/UnprocessedEntitiesModule.ts b/plugins/catalog-backend-module-unprocessed/src/UnprocessedEntitiesModule.ts
new file mode 100644
index 0000000000..6ea184ce83
--- /dev/null
+++ b/plugins/catalog-backend-module-unprocessed/src/UnprocessedEntitiesModule.ts
@@ -0,0 +1,132 @@
+/*
+ * 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 {
+ HydratedRefreshState,
+ RefreshState,
+ UnprocessedEntitiesRequest,
+ UnprocessedEntitiesResponse,
+} from './types';
+import { Knex } from 'knex';
+import { HttpRouterService } from '@backstage/backend-plugin-api';
+import Router from 'express-promise-router';
+import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
+
+/**
+ * Module providing Unprocessed Entities API endpoints
+ *
+ * @public
+ */
+export class UnprocessedEntitesModule {
+ private readonly moduleRouter;
+
+ constructor(
+ private readonly database: Knex,
+ private readonly router: HttpRouterService,
+ ) {
+ this.moduleRouter = Router();
+ this.router.use(this.moduleRouter);
+ }
+
+ private async unprocessed(
+ request: UnprocessedEntitiesRequest,
+ ): Promise {
+ if (request.reason === 'pending') {
+ return {
+ type: 'pending',
+ entities: await this.pending(request.owner),
+ };
+ }
+ return {
+ type: 'failed',
+ entities: await this.failed(request.owner),
+ };
+ }
+
+ private hydrateRefreshState(r: RefreshState): HydratedRefreshState {
+ return {
+ ...r,
+ unprocessed_entity: JSON.parse(r.unprocessed_entity),
+ ...(r.processed_entity && {
+ processed_entity: JSON.parse(r.processed_entity),
+ }),
+ ...(r.errors && { errors: JSON.parse(r.errors) }),
+ ...(r.cache && { cache: JSON.parse(r.cache) }),
+ };
+ }
+
+ private async pending(owner?: string): Promise {
+ const res = (
+ await this.database('refresh_state.*')
+ .from('refresh_state')
+ .leftJoin(
+ 'final_entities',
+ 'final_entities.entity_id',
+ 'refresh_state.entity_id',
+ )
+ .whereNull('final_entities.entity_id')
+ ).map(this.hydrateRefreshState);
+ if (owner) {
+ return res.filter(r => r.unprocessed_entity.spec?.owner === owner);
+ }
+
+ return res;
+ }
+
+ private async failed(owner?: string): Promise {
+ const res = (
+ await this.database('refresh_state.*')
+ .from('refresh_state')
+ .rightJoin(
+ 'final_entities',
+ 'final_entities.entity_id',
+ 'refresh_state.entity_id',
+ )
+ .whereNull('final_entities.final_entity')
+ ).map(this.hydrateRefreshState);
+ if (owner) {
+ return res.filter(r => r.unprocessed_entity.spec?.owner === owner);
+ }
+
+ return res;
+ }
+
+ registerRoutes() {
+ this.moduleRouter
+ .get('/entities/unprocessed/failed', async (req, res) => {
+ return res.json(
+ await this.unprocessed({
+ reason: 'failed',
+ owner: req.query.owner as string,
+ authorizationToken: getBearerTokenFromAuthorizationHeader(
+ req.header('authorization'),
+ ),
+ }),
+ );
+ })
+ .get('/entities/unprocessed/pending', async (req, res) => {
+ return res.json(
+ await this.unprocessed({
+ reason: 'pending',
+ owner: req.query.owner as string,
+ authorizationToken: getBearerTokenFromAuthorizationHeader(
+ req.header('authorization'),
+ ),
+ }),
+ );
+ });
+ }
+}
diff --git a/plugins/catalog-backend-module-unprocessed/src/index.ts b/plugins/catalog-backend-module-unprocessed/src/index.ts
new file mode 100644
index 0000000000..21cb2dbcd6
--- /dev/null
+++ b/plugins/catalog-backend-module-unprocessed/src/index.ts
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+/**
+ * Node.js library for the catalog-backend-module-unprocessed plugin.
+ *
+ * @packageDocumentation
+ */
+
+export * from './module';
+export * from './UnprocessedEntitiesModule';
diff --git a/plugins/catalog-backend-module-unprocessed/src/module.ts b/plugins/catalog-backend-module-unprocessed/src/module.ts
new file mode 100644
index 0000000000..1c4b6c658f
--- /dev/null
+++ b/plugins/catalog-backend-module-unprocessed/src/module.ts
@@ -0,0 +1,51 @@
+/*
+ * 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 {
+ coreServices,
+ createBackendModule,
+} from '@backstage/backend-plugin-api';
+import { UnprocessedEntitesModule } from './UnprocessedEntitiesModule';
+
+/**
+ * Catalog Module for Unprocessed Entities
+ *
+ * @public
+ */
+export const catalogModuleUnprocessedEntities = createBackendModule({
+ pluginId: 'catalog',
+ moduleId: 'catalogModuleUnprocessedEntities',
+ register(env) {
+ env.registerInit({
+ deps: {
+ database: coreServices.database,
+ router: coreServices.httpRouter,
+ logger: coreServices.logger,
+ },
+ async init({ database, router, logger }) {
+ const module = new UnprocessedEntitesModule(
+ await database.getClient(),
+ router,
+ );
+
+ module.registerRoutes();
+ logger.info(
+ 'registered additional routes for catalogModuleUnprocessedEntities',
+ );
+ },
+ });
+ },
+});
diff --git a/plugins/catalog-backend-module-unprocessed/src/setupTests.ts b/plugins/catalog-backend-module-unprocessed/src/setupTests.ts
new file mode 100644
index 0000000000..4b9026cde5
--- /dev/null
+++ b/plugins/catalog-backend-module-unprocessed/src/setupTests.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 {};
diff --git a/plugins/catalog-backend-module-unprocessed/src/types.ts b/plugins/catalog-backend-module-unprocessed/src/types.ts
new file mode 100644
index 0000000000..8d70345682
--- /dev/null
+++ b/plugins/catalog-backend-module-unprocessed/src/types.ts
@@ -0,0 +1,69 @@
+/*
+ * 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 { Entity } from '@backstage/catalog-model';
+
+export type HydratedRefreshState = {
+ entity_id: string;
+ entity_ref: string;
+ unprocessed_entity: Entity;
+ unprocessed_hash?: string;
+ processed_entity?: Entity;
+ result_hash?: string;
+ cache?: RefreshStateCache;
+ next_update_at: string | Date;
+ last_discovery_at: string | Date; // remove?
+ errors?: RefreshStateError[];
+ location_key?: string;
+};
+
+export type RefreshState = {
+ entity_id: string;
+ entity_ref: string;
+ unprocessed_entity: string;
+ unprocessed_hash?: string;
+ processed_entity?: string;
+ result_hash?: string;
+ cache?: string;
+ next_update_at: string | Date;
+ last_discovery_at: string | Date; // remove?
+ errors?: string;
+ location_key?: string;
+};
+export type RefreshStateCache = {
+ ttl: number;
+ cache: object;
+};
+
+export type RefreshStateError = {
+ name: string;
+ message: string;
+ cause: {
+ name: string;
+ message: string;
+ stack: string;
+ };
+};
+
+export interface UnprocessedEntitiesRequest {
+ reason: 'failed' | 'pending';
+ owner?: string;
+ authorizationToken?: string;
+}
+
+export interface UnprocessedEntitiesResponse {
+ type: 'pending' | 'failed';
+ entities: HydratedRefreshState[];
+}
diff --git a/plugins/catalog-unprocessed-entities/.eslintrc.js b/plugins/catalog-unprocessed-entities/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/catalog-unprocessed-entities/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/catalog-unprocessed-entities/README.md b/plugins/catalog-unprocessed-entities/README.md
new file mode 100644
index 0000000000..fd824bec94
--- /dev/null
+++ b/plugins/catalog-unprocessed-entities/README.md
@@ -0,0 +1,29 @@
+# catalog-unprocessed-entities
+
+Frontend plugin to view unprocessed entities.
+
+## Requirements
+
+Requires the `@backstage/plugin-catalog-backend-module-unprocessed` module to be installed.
+
+## Installation
+
+Import into your App.tsx and include into the `` component:
+
+```tsx
+import { CatalogUnprocessedEntitiesPage } from '@backstage/plugin-catalog-unprocessed-entities';
+//...
+
+}
+/>;
+```
+
+## Getting started
+
+Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/catalog-unprocessed-entities](http://localhost:3000/catalog-unprocessed-entities).
+
+You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
+This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
+It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
diff --git a/plugins/catalog-unprocessed-entities/api-report.md b/plugins/catalog-unprocessed-entities/api-report.md
new file mode 100644
index 0000000000..b1e0d6f53d
--- /dev/null
+++ b/plugins/catalog-unprocessed-entities/api-report.md
@@ -0,0 +1,24 @@
+## API Report File for "@backstage/plugin-catalog-unprocessed-entities"
+
+> 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 { RouteRef } from '@backstage/core-plugin-api';
+
+// @public
+export const CatalogUnprocessedEntitiesPage: () => JSX.Element;
+
+// @public
+export const catalogUnprocessedEntitiesPlugin: BackstagePlugin<
+ {
+ root: RouteRef;
+ },
+ {},
+ {}
+>;
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/catalog-unprocessed-entities/dev/index.tsx b/plugins/catalog-unprocessed-entities/dev/index.tsx
new file mode 100644
index 0000000000..ab11cb554d
--- /dev/null
+++ b/plugins/catalog-unprocessed-entities/dev/index.tsx
@@ -0,0 +1,30 @@
+/*
+ * 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 React from 'react';
+import { createDevApp } from '@backstage/dev-utils';
+import {
+ catalogUnprocessedEntitiesPlugin,
+ CatalogUnprocessedEntitiesPage,
+} from '../src/plugin';
+
+createDevApp()
+ .registerPlugin(catalogUnprocessedEntitiesPlugin)
+ .addPage({
+ element: ,
+ title: 'Root Page',
+ path: '/catalog-unprocessed-entities',
+ })
+ .render();
diff --git a/plugins/catalog-unprocessed-entities/package.json b/plugins/catalog-unprocessed-entities/package.json
new file mode 100644
index 0000000000..9a05c7e8cb
--- /dev/null
+++ b/plugins/catalog-unprocessed-entities/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@backstage/plugin-catalog-unprocessed-entities",
+ "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"
+ },
+ "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/theme": "workspace:^",
+ "@material-ui/core": "^4.9.13",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "^4.0.0-alpha.60",
+ "react-use": "^17.2.4"
+ },
+ "peerDependencies": {
+ "react": "^16.13.1 || ^17.0.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": "*",
+ "cross-fetch": "^3.1.5",
+ "msw": "^1.0.0"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/catalog-unprocessed-entities/src/api/index.ts b/plugins/catalog-unprocessed-entities/src/api/index.ts
new file mode 100644
index 0000000000..a4f2294aac
--- /dev/null
+++ b/plugins/catalog-unprocessed-entities/src/api/index.ts
@@ -0,0 +1,59 @@
+/*
+ * 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 { DiscoveryApi, createApiRef } from '@backstage/core-plugin-api';
+import { ResponseError } from '@backstage/errors';
+import { UnprocessedEntity } from '../types';
+
+/**
+ * {@link @backstage/core-plugin-api#ApiRef} for the {@link CatalogUnprocessedEntitiesApi}
+ *
+ * @public
+ */
+export const catalogUnprocessedEntitiesApiRef =
+ createApiRef({
+ id: 'plugin.catalog-unprocessed-entities.service',
+ });
+
+/**
+ * API client for the Catalog Unprocessed Entities plugin
+ *
+ * @public
+ */
+export class CatalogUnprocessedEntitiesApi {
+ url: string = '';
+
+ constructor(public discovery: DiscoveryApi) {}
+
+ private async fetch(path: string, init?: RequestInit): Promise {
+ if (!this.url) {
+ this.url = await this.discovery.getBaseUrl('catalog');
+ }
+ const resp = await fetch(`${this.url}/${path}`, init);
+ if (!resp.ok) {
+ throw await ResponseError.fromResponse(resp);
+ }
+
+ return await resp.json();
+ }
+
+ async pending(): Promise<{ entities: UnprocessedEntity[] }> {
+ return await this.fetch('entities/unprocessed/pending');
+ }
+
+ async failed(): Promise<{ entities: UnprocessedEntity[] }> {
+ return await this.fetch('entities/unprocessed/failed');
+ }
+}
diff --git a/plugins/catalog-unprocessed-entities/src/components/EntityDialog.tsx b/plugins/catalog-unprocessed-entities/src/components/EntityDialog.tsx
new file mode 100644
index 0000000000..a02d8419d4
--- /dev/null
+++ b/plugins/catalog-unprocessed-entities/src/components/EntityDialog.tsx
@@ -0,0 +1,89 @@
+/*
+ * 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 React, { useState } from 'react';
+
+import Dialog from '@material-ui/core/Dialog';
+import DialogContent from '@material-ui/core/DialogContent';
+import DialogTitle from '@material-ui/core/DialogTitle';
+import IconButton from '@material-ui/core/IconButton';
+import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
+import CloseIcon from '@material-ui/icons/Close';
+import DescriptionIcon from '@material-ui/icons/Description';
+
+import { UnprocessedEntity } from './../types';
+import { CodeSnippet } from '@backstage/core-components';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ closeButton: {
+ position: 'absolute',
+ right: theme.spacing(1),
+ top: theme.spacing(1),
+ color: theme.palette.grey[500],
+ },
+ entity: {
+ overflow: 'scroll',
+ width: '100%',
+ },
+ codeBox: {
+ border: '1px solid black',
+ padding: '1em',
+ },
+ }),
+);
+
+export const EntityDialog = ({ entity }: { entity: UnprocessedEntity }) => {
+ const [open, setOpen] = useState(false);
+ const classes = useStyles();
+
+ const openDialog = () => {
+ setOpen(true);
+ };
+
+ const closeDialog = () => {
+ setOpen(false);
+ };
+
+ const dialogContent = () => {
+ return (
+
+ );
+ };
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
diff --git a/plugins/catalog-unprocessed-entities/src/components/FailedEntities.tsx b/plugins/catalog-unprocessed-entities/src/components/FailedEntities.tsx
new file mode 100644
index 0000000000..44a146f027
--- /dev/null
+++ b/plugins/catalog-unprocessed-entities/src/components/FailedEntities.tsx
@@ -0,0 +1,161 @@
+/*
+ * 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 React from 'react';
+
+import {
+ ErrorPanel,
+ MarkdownContent,
+ Progress,
+ Table,
+ TableColumn,
+} from '@backstage/core-components';
+import { useApi } from '@backstage/core-plugin-api';
+import { BackstageThemeOptions } from '@backstage/theme';
+import { Box, Typography, makeStyles } from '@material-ui/core';
+
+import { UnprocessedEntity } from '../types';
+import { EntityDialog } from './EntityDialog';
+import { catalogUnprocessedEntitiesApiRef } from '../api';
+import useAsync from 'react-use/lib/useAsync';
+
+const useStyles = makeStyles((theme: BackstageThemeOptions) => ({
+ errorBox: {
+ color: theme.palette.status.error,
+ backgroundColor: theme.palette.errorBackground,
+ padding: '1em',
+ margin: '1em',
+ border: `1px solid ${theme.palette.status.error}`,
+ },
+ errorTitle: {
+ width: '100%',
+ fontWeight: 'bold',
+ },
+ successMessage: {
+ background: theme.palette.infoBackground,
+ color: theme.palette.infoText,
+ },
+}));
+
+const RenderErrorContext = ({
+ error,
+ rowData,
+}: {
+ error: { message: string };
+ rowData: UnprocessedEntity;
+}) => {
+ if (error.message.includes('tags.')) {
+ return (
+ <>
+ Tags
+
+ {rowData.unprocessed_entity.metadata.tags?.map(t => (
+ - {t}
+ ))}
+
+ >
+ );
+ }
+
+ if (error.message.includes('metadata.name')) {
+ return (
+ <>
+ Name
+
+ {rowData.unprocessed_entity.metadata.name}
+
+ >
+ );
+ }
+
+ return null;
+};
+
+export const FailedEntities = () => {
+ const classes = useStyles();
+ const unprocessedApi = useApi(catalogUnprocessedEntitiesApiRef);
+ const {
+ loading,
+ error,
+ value: data,
+ } = useAsync(async () => await unprocessedApi.failed());
+
+ if (loading) {
+ return ;
+ }
+ if (error) {
+ return ;
+ }
+
+ const columns: TableColumn[] = [
+ {
+ title: entityRef,
+ render: (rowData: UnprocessedEntity | {}) =>
+ (rowData as UnprocessedEntity).entity_ref,
+ },
+ {
+ title: Kind,
+ render: (rowData: UnprocessedEntity | {}) =>
+ (rowData as UnprocessedEntity).unprocessed_entity.kind,
+ },
+ {
+ title: Owner,
+ render: (rowData: UnprocessedEntity | {}) =>
+ (rowData as UnprocessedEntity).unprocessed_entity.spec?.owner ||
+ 'unknown',
+ },
+ {
+ title: Raw,
+ render: (rowData: UnprocessedEntity | {}) => (
+
+ ),
+ },
+ ];
+ return (
+ <>
+
+ No failed entities found
+
+ }
+ detailPanel={({ rowData }) => {
+ const errors = (rowData as UnprocessedEntity).errors;
+ return (
+ <>
+ {errors?.map(e => {
+ return (
+
+
+ {e.name}
+
+
+
+
+ );
+ })}
+ >
+ );
+ }}
+ />
+ >
+ );
+};
diff --git a/plugins/catalog-unprocessed-entities/src/components/PendingEntities.tsx b/plugins/catalog-unprocessed-entities/src/components/PendingEntities.tsx
new file mode 100644
index 0000000000..88ace1cfb0
--- /dev/null
+++ b/plugins/catalog-unprocessed-entities/src/components/PendingEntities.tsx
@@ -0,0 +1,96 @@
+/*
+ * 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 React from 'react';
+
+import {
+ ErrorPanel,
+ Progress,
+ TableColumn,
+ Table,
+} from '@backstage/core-components';
+import { Typography, makeStyles } from '@material-ui/core';
+
+import { UnprocessedEntity } from '../types';
+
+import { EntityDialog } from './EntityDialog';
+import { useApi } from '@backstage/core-plugin-api';
+import useAsync from 'react-use/lib/useAsync';
+import { catalogUnprocessedEntitiesApiRef } from '../api';
+import { BackstageTheme } from '@backstage/theme';
+
+const useStyles = makeStyles((theme: BackstageTheme) => ({
+ successMessage: {
+ background: theme.palette.infoBackground,
+ color: theme.palette.infoText,
+ padding: theme.spacing(2),
+ },
+}));
+
+export const PendingEntities = () => {
+ const classes = useStyles();
+ const unprocessedApi = useApi(catalogUnprocessedEntitiesApiRef);
+ const {
+ loading,
+ error,
+ value: data,
+ } = useAsync(async () => await unprocessedApi.pending());
+
+ if (loading) {
+ return ;
+ }
+ if (error) {
+ return ;
+ }
+
+ const columns: TableColumn[] = [
+ {
+ title: entityRef,
+ render: (rowData: UnprocessedEntity | {}) =>
+ (rowData as UnprocessedEntity).entity_ref,
+ },
+ {
+ title: Kind,
+ render: (rowData: UnprocessedEntity | {}) =>
+ (rowData as UnprocessedEntity).unprocessed_entity.kind,
+ },
+ {
+ title: Owner,
+ render: (rowData: UnprocessedEntity | {}) =>
+ (rowData as UnprocessedEntity).unprocessed_entity.spec?.owner ||
+ 'unknown',
+ },
+ {
+ title: Raw,
+ render: (rowData: UnprocessedEntity | {}) => (
+
+ ),
+ },
+ ];
+ return (
+ <>
+
+ No pending entities found
+
+ }
+ />
+ >
+ );
+};
diff --git a/plugins/catalog-unprocessed-entities/src/components/UnprocessedEntities.tsx b/plugins/catalog-unprocessed-entities/src/components/UnprocessedEntities.tsx
new file mode 100644
index 0000000000..ba186888ec
--- /dev/null
+++ b/plugins/catalog-unprocessed-entities/src/components/UnprocessedEntities.tsx
@@ -0,0 +1,56 @@
+/*
+ * 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 React, { useState } from 'react';
+
+import { Page, Header, Content } from '@backstage/core-components';
+import { Tab } from '@material-ui/core';
+import { TabContext, TabList, TabPanel } from '@material-ui/lab';
+
+import { FailedEntities } from './FailedEntities';
+import { PendingEntities } from './PendingEntities';
+
+export const UnprocessedEntitiesContent = () => {
+ const [tab, setTab] = useState('failed');
+ const handleChange = (_event: React.ChangeEvent<{}>, tabValue: string) => {
+ setTab(tabValue);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const UnprocessedEntities = () => {
+ return (
+
+
+
+
+ );
+};
diff --git a/plugins/catalog-unprocessed-entities/src/index.ts b/plugins/catalog-unprocessed-entities/src/index.ts
new file mode 100644
index 0000000000..a65283926e
--- /dev/null
+++ b/plugins/catalog-unprocessed-entities/src/index.ts
@@ -0,0 +1,19 @@
+/*
+ * 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 {
+ catalogUnprocessedEntitiesPlugin,
+ CatalogUnprocessedEntitiesPage,
+} from './plugin';
diff --git a/plugins/catalog-unprocessed-entities/src/plugin.test.ts b/plugins/catalog-unprocessed-entities/src/plugin.test.ts
new file mode 100644
index 0000000000..add3d677f0
--- /dev/null
+++ b/plugins/catalog-unprocessed-entities/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 { catalogUnprocessedEntitiesPlugin } from './plugin';
+
+describe('catalog-unprocessed-entities', () => {
+ it('should export plugin', () => {
+ expect(catalogUnprocessedEntitiesPlugin).toBeDefined();
+ });
+});
diff --git a/plugins/catalog-unprocessed-entities/src/plugin.ts b/plugins/catalog-unprocessed-entities/src/plugin.ts
new file mode 100644
index 0000000000..88d3ed45ea
--- /dev/null
+++ b/plugins/catalog-unprocessed-entities/src/plugin.ts
@@ -0,0 +1,64 @@
+/*
+ * 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,
+} from '@backstage/core-plugin-api';
+
+import { rootRouteRef } from './routes';
+import {
+ CatalogUnprocessedEntitiesApi,
+ catalogUnprocessedEntitiesApiRef,
+} from './api';
+
+/**
+ * Plugin entry point
+ *
+ * @public
+ */
+export const catalogUnprocessedEntitiesPlugin = createPlugin({
+ id: 'catalog-unprocessed-entities',
+ routes: {
+ root: rootRouteRef,
+ },
+ apis: [
+ createApiFactory({
+ api: catalogUnprocessedEntitiesApiRef,
+ deps: { discoveryApi: discoveryApiRef },
+ factory: ({ discoveryApi }) =>
+ new CatalogUnprocessedEntitiesApi(discoveryApi),
+ }),
+ ],
+});
+
+/**
+ * Tool page for the Catalog Unprocessed Entities Plugin
+ *
+ * @public
+ */
+export const CatalogUnprocessedEntitiesPage =
+ catalogUnprocessedEntitiesPlugin.provide(
+ createRoutableExtension({
+ name: 'CatalogUnprocessedEntitiesPage',
+ component: () =>
+ import('./components/UnprocessedEntities').then(
+ m => m.UnprocessedEntities,
+ ),
+ mountPoint: rootRouteRef,
+ }),
+ );
diff --git a/plugins/catalog-unprocessed-entities/src/routes.ts b/plugins/catalog-unprocessed-entities/src/routes.ts
new file mode 100644
index 0000000000..1bd268c392
--- /dev/null
+++ b/plugins/catalog-unprocessed-entities/src/routes.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 } from '@backstage/core-plugin-api';
+
+export const rootRouteRef = createRouteRef({
+ id: 'catalog-unprocessed-entities',
+});
diff --git a/plugins/catalog-unprocessed-entities/src/setupTests.ts b/plugins/catalog-unprocessed-entities/src/setupTests.ts
new file mode 100644
index 0000000000..73dd8dce47
--- /dev/null
+++ b/plugins/catalog-unprocessed-entities/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/plugins/catalog-unprocessed-entities/src/types.ts b/plugins/catalog-unprocessed-entities/src/types.ts
new file mode 100644
index 0000000000..490027b57a
--- /dev/null
+++ b/plugins/catalog-unprocessed-entities/src/types.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { Entity } from '@backstage/catalog-model';
+
+export type UnprocessedEntity = {
+ entity_id: string;
+ entity_ref: string;
+ unprocessed_entity: Entity;
+ unprocessed_hash?: string;
+ processed_entity?: Entity;
+ result_hash?: string;
+ cache?: UnprocessedEntityCache;
+ next_update_at: string | Date;
+ last_discovery_at: string | Date; // remove?
+ errors?: UnprocessedEntityError[];
+ location_key?: string;
+};
+
+export type UnprocessedEntityCache = {
+ ttl: number;
+ cache: object;
+};
+
+export type UnprocessedEntityError = {
+ name: string;
+ message: string;
+ cause: {
+ name: string;
+ message: string;
+ stack: string;
+ };
+};
diff --git a/yarn.lock b/yarn.lock
index a6a300e49b..87399900be 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5609,6 +5609,19 @@ __metadata:
languageName: unknown
linkType: soft
+"@backstage/plugin-catalog-backend-module-unprocessed@workspace:^, @backstage/plugin-catalog-backend-module-unprocessed@workspace:plugins/catalog-backend-module-unprocessed":
+ version: 0.0.0-use.local
+ resolution: "@backstage/plugin-catalog-backend-module-unprocessed@workspace:plugins/catalog-backend-module-unprocessed"
+ dependencies:
+ "@backstage/backend-plugin-api": "workspace:^"
+ "@backstage/catalog-model": "workspace:^"
+ "@backstage/cli": "workspace:^"
+ "@backstage/plugin-auth-node": "workspace:^"
+ express-promise-router: ^4.1.1
+ knex: ^2.4.2
+ languageName: unknown
+ linkType: soft
+
"@backstage/plugin-catalog-backend@workspace:^, @backstage/plugin-catalog-backend@workspace:plugins/catalog-backend":
version: 0.0.0-use.local
resolution: "@backstage/plugin-catalog-backend@workspace:plugins/catalog-backend"
@@ -5891,6 +5904,34 @@ __metadata:
languageName: unknown
linkType: soft
+"@backstage/plugin-catalog-unprocessed-entities@workspace:^, @backstage/plugin-catalog-unprocessed-entities@workspace:plugins/catalog-unprocessed-entities":
+ version: 0.0.0-use.local
+ resolution: "@backstage/plugin-catalog-unprocessed-entities@workspace:plugins/catalog-unprocessed-entities"
+ 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/test-utils": "workspace:^"
+ "@backstage/theme": "workspace:^"
+ "@material-ui/core": ^4.9.13
+ "@material-ui/icons": ^4.9.1
+ "@material-ui/lab": ^4.0.0-alpha.60
+ "@testing-library/jest-dom": ^5.10.1
+ "@testing-library/react": ^12.1.3
+ "@testing-library/user-event": ^14.0.0
+ "@types/node": "*"
+ cross-fetch: ^3.1.5
+ msw: ^1.0.0
+ react-use: ^17.2.4
+ peerDependencies:
+ react: ^16.13.1 || ^17.0.0
+ languageName: unknown
+ linkType: soft
+
"@backstage/plugin-catalog@workspace:^, @backstage/plugin-catalog@workspace:plugins/catalog":
version: 0.0.0-use.local
resolution: "@backstage/plugin-catalog@workspace:plugins/catalog"
@@ -24341,6 +24382,7 @@ __metadata:
"@backstage/plugin-catalog-graph": "workspace:^"
"@backstage/plugin-catalog-import": "workspace:^"
"@backstage/plugin-catalog-react": "workspace:^"
+ "@backstage/plugin-catalog-unprocessed-entities": "workspace:^"
"@backstage/plugin-circleci": "workspace:^"
"@backstage/plugin-cloudbuild": "workspace:^"
"@backstage/plugin-code-coverage": "workspace:^"
@@ -24433,6 +24475,7 @@ __metadata:
"@backstage/plugin-app-backend": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
"@backstage/plugin-catalog-backend": "workspace:^"
+ "@backstage/plugin-catalog-backend-module-unprocessed": "workspace:^"
"@backstage/plugin-kubernetes-backend": "workspace:^"
"@backstage/plugin-permission-backend": "workspace:^"
"@backstage/plugin-permission-common": "workspace:^"
@@ -24467,6 +24510,7 @@ __metadata:
"@backstage/plugin-azure-sites-backend": "workspace:^"
"@backstage/plugin-badges-backend": "workspace:^"
"@backstage/plugin-catalog-backend": "workspace:^"
+ "@backstage/plugin-catalog-backend-module-unprocessed": "workspace:^"
"@backstage/plugin-catalog-node": "workspace:^"
"@backstage/plugin-code-coverage-backend": "workspace:^"
"@backstage/plugin-devtools-backend": "workspace:^"
@@ -24642,7 +24686,7 @@ __metadata:
languageName: node
linkType: hard
-"express-promise-router@npm:^4.1.0":
+"express-promise-router@npm:^4.1.0, express-promise-router@npm:^4.1.1":
version: 4.1.1
resolution: "express-promise-router@npm:4.1.1"
dependencies: