diff --git a/.github/workflows/release-please.yaml b/.github/workflows/release-please.yaml
index b63dffa..a39efd6 100644
--- a/.github/workflows/release-please.yaml
+++ b/.github/workflows/release-please.yaml
@@ -2,6 +2,11 @@ on:
push:
branches:
- main
+
+permissions:
+ contents: write
+ pull-requests: write
+
name: release-please
jobs:
release-please:
diff --git a/README.md b/README.md
index d359da0..b9cdd7c 100644
--- a/README.md
+++ b/README.md
@@ -31,14 +31,14 @@ In your `EntityPage.tsx` file located in `packages\app\src\components\catalog` w
First we need to add the following imports:
```ts
-import { DockerRepositoriesWidget } from '@workm8/backstage-docker-plugin';
+import { DockerTagsTableWidget } from '@workm8/backstage-docker-plugin';
```
You can display the Widget by adding the following code (for example, the `overviewContent`):
```diff
-+ const dockerImagesContent = (
-+
+ );
@@ -53,7 +53,7 @@ const overviewContent = (
+
-+ {dockerImagesContent}
++ {dockerTagsContent}
+
diff --git a/src/apis/Docker/DockerApi.ts b/src/apis/Docker/DockerApi.ts
index 9d9c012..960f799 100644
--- a/src/apis/Docker/DockerApi.ts
+++ b/src/apis/Docker/DockerApi.ts
@@ -16,13 +16,23 @@ export class DockerClient implements DockerApi {
pageNumber: number,
pageSize: number,
): Promise {
- console.log(this.options.configApi);
const baseUrl = await this.options.discoveryApi.getBaseUrl('');
const targetUrl = `${baseUrl}proxy${url}`;
- return this.options.fetchApi
- .fetch(`${targetUrl}?page=${pageNumber}&page_size=${pageSize}`)
- .then(res => res.json());
+ return new Promise((resolve, reject) => {
+ this.options.fetchApi
+ .fetch(`${targetUrl}?page=${pageNumber}&page_size=${pageSize}`)
+ .then(res => res.json())
+ .then(res => {
+ if ('errinfo' in res) {
+ return reject({
+ name: 'Error',
+ message: `Could not find namespace ${res.errinfo.namespace} or repository ${res.errinfo.repository}`,
+ });
+ }
+ return resolve(res);
+ });
+ });
}
}
diff --git a/src/apis/Docker/types.ts b/src/apis/Docker/types.ts
index 11fe90b..ebf664a 100644
--- a/src/apis/Docker/types.ts
+++ b/src/apis/Docker/types.ts
@@ -19,10 +19,10 @@ export type Image = {
export type Repository = {
creator: number;
id: number;
- images: Image[];
+ images: Partial[];
last_updated: string;
last_updater: number;
- last_updated_username: string;
+ last_updater_username: string;
name: string;
repository: number;
full_size: number;
@@ -39,7 +39,7 @@ export type TagsResponse = {
count: number;
next?: string;
previous?: string;
- results: Repository[];
+ results: Partial[];
};
export interface DockerApi {
diff --git a/src/components/Docker/DockerTagsTable.test.tsx b/src/components/Docker/DockerTagsTable.test.tsx
new file mode 100644
index 0000000..127c319
--- /dev/null
+++ b/src/components/Docker/DockerTagsTable.test.tsx
@@ -0,0 +1,247 @@
+import React from 'react';
+import { DockerTagsTable } from './DockerTagsTable';
+import {
+ renderInTestApp,
+ setupRequestMockHandlers,
+ TestApiProvider
+} from '@backstage/test-utils';
+
+import { Entity } from '@backstage/catalog-model';
+import { EntityProvider } from '@backstage/plugin-catalog-react';
+
+import { setupServer } from 'msw/node';
+
+import { DockerApi, dockerApiRef } from '../../apis';
+
+describe('DockerTagsTable', () => {
+ const worker = setupServer();
+ setupRequestMockHandlers(worker);
+
+ const dockerApi: jest.Mocked = {
+ getRepositories: jest.fn()
+ };
+
+ let Wrapper: React.ComponentType>;
+
+ beforeEach(() => {
+ Wrapper = ({ children }: { children?: React.ReactNode }) => (
+
+ {children}
+
+ );
+ });
+
+ it('renders missing Annotation error', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'my-name',
+ },
+ };
+ const widget = await renderInTestApp(
+
+
+
+
+
+ )
+ expect(widget.getByText(/Missing Annotation/i)).toBeInTheDocument();
+ expect(widget.getByText('docker.com/repository')).toBeInTheDocument();
+ });
+
+ it('renders basic table', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'my-name',
+ annotations: {
+ 'docker.com/repository': 'foo/bar'
+ }
+ },
+ };
+
+ dockerApi.getRepositories.mockResolvedValue({
+ count: 1,
+ results: [
+ {
+ name: 'V1.0.0',
+ tag_status: 'Active',
+ last_updater_username: 'TEST_USERNAME',
+ images: [
+ {
+ architecture: 'AMD64'
+ }
+ ]
+ }
+ ]
+ });
+
+ const widget = await renderInTestApp(
+
+
+
+
+
+ )
+
+ expect(widget.getByText('Docker Tags (1)')).toBeInTheDocument();
+ expect(widget.getByText('V1.0.0')).toBeInTheDocument();
+ expect(widget.getByText('Active')).toBeInTheDocument();
+ expect(widget.getByText('TEST_USERNAME')).toBeInTheDocument();
+ expect(widget.getByText('AMD64')).toBeInTheDocument();
+ });
+
+ ['name', 'status', 'username', 'architecture'].forEach((column) => {
+ it(`renders table with column ${column}`, async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'my-name',
+ annotations: {
+ 'docker.com/repository': 'foo/bar'
+ }
+ },
+ };
+
+ dockerApi.getRepositories.mockResolvedValue({
+ count: 1,
+ results: [
+ {
+ name: 'V1.0.0',
+ tag_status: 'Active',
+ last_updater_username: 'TEST_USERNAME',
+ images: [
+ {
+ architecture: 'AMD64'
+ }
+ ]
+ }
+ ]
+ });
+
+ const widget = await renderInTestApp(
+
+
+
+
+
+ )
+
+ expect(widget.getByText('Docker Tags (1)')).toBeInTheDocument();
+
+ switch (column) {
+ case 'name':
+ expect(widget.getByText('V1.0.0')).toBeInTheDocument();
+ break;
+ case 'status':
+ expect(widget.getByText('Active')).toBeInTheDocument();
+ break;
+ case 'username':
+ expect(widget.getByText('TEST_USERNAME')).toBeInTheDocument();
+ break;
+ case 'architecture':
+ expect(widget.getByText('AMD64')).toBeInTheDocument();
+ break;
+ }
+ });
+ });
+
+ it('renders empty table', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'my-name',
+ annotations: {
+ 'docker.com/repository': 'foo/bar'
+ }
+ },
+ };
+
+ dockerApi.getRepositories.mockResolvedValue({
+ count: 0,
+ results: []
+ });
+
+ const widget = await renderInTestApp(
+
+
+
+
+
+ )
+ expect(widget.getByText('Docker Tags (0)')).toBeInTheDocument();
+ expect(widget.getByText('No records to display')).toBeInTheDocument();
+ });
+
+ it('renders a table with mock data', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'my-name',
+ annotations: {
+ 'docker.com/repository': 'foo/bar',
+ },
+ },
+ };
+
+ const columns = [
+ {
+ name: 'V1.0.0',
+ tag_status: 'Active',
+ last_updater_username: 'TEST',
+ },
+ {
+ name: 'V0.9.0',
+ tag_status: 'Inactive',
+ last_updater_username: 'TEST',
+ },
+ ];
+ dockerApi.getRepositories.mockResolvedValue({
+ count: 2,
+ results: columns,
+ });
+
+ const widget = await renderInTestApp(
+
+
+
+
+ ,
+ );
+
+ expect(widget.getByText('Docker Tags (2)')).toBeInTheDocument();
+ });
+
+ it('Renders custom header', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'my-name',
+ annotations: {
+ 'docker.com/repository': 'foo/bar'
+ }
+ },
+ };
+
+ dockerApi.getRepositories.mockResolvedValue({
+ count: 0,
+ results: []
+ });
+
+ const widget = await renderInTestApp(
+
+
+
+
+
+ )
+ expect(widget.getByText('Tags (0)')).toBeInTheDocument();
+ });
+
+});
\ No newline at end of file
diff --git a/src/components/DockerComponent/DockerComponent.tsx b/src/components/Docker/DockerTagsTable.tsx
similarity index 78%
rename from src/components/DockerComponent/DockerComponent.tsx
rename to src/components/Docker/DockerTagsTable.tsx
index e4a9eac..135a239 100644
--- a/src/components/DockerComponent/DockerComponent.tsx
+++ b/src/components/Docker/DockerTagsTable.tsx
@@ -1,20 +1,15 @@
import React, { useMemo, useState } from 'react';
-import {
- MissingAnnotationEmptyState
-} from '@backstage/core-components';
-import { Entity } from '@backstage/catalog-model';
+import { MissingAnnotationEmptyState, ErrorPanel } from '@backstage/core-components';
+import { Entity } from '@backstage/catalog-model';
import { useApi } from '@backstage/core-plugin-api';
-
-import { dockerApiRef, Repository } from '../../apis';
-
+import { Table, TableColumn } from '@backstage/core-components';
import { useEntity } from '@backstage/plugin-catalog-react';
import { Box, Chip } from '@material-ui/core';
-import {
- Table,
- TableColumn,
-} from '@backstage/core-components';
+import Typography from '@material-ui/core/Typography';
+
+import { dockerApiRef, Repository } from '../../apis';
export const ANNOTATION_DOCKER_REPOSITORY = 'docker.com/repository';
@@ -43,8 +38,7 @@ const getDockerRepositoryUrl = (
}
};
-const getColumns = (options: DockerImagesTableProps) => {
-
+const getColumns = (options: DockerTagsTableProps) => {
const columns: TableColumn[] = [];
if ((options.columns || []).includes('name')) {
@@ -104,7 +98,7 @@ const getColumns = (options: DockerImagesTableProps) => {
return columns;
}
-export interface DockerImagesTableProps {
+export interface DockerTagsTableProps {
heading: string;
columns: string[];
initialPage: number;
@@ -113,8 +107,8 @@ export interface DockerImagesTableProps {
showCountInHeading: boolean;
}
-const DEFAULT_DOCKER_IMAGES_TABLE_PROPS: DockerImagesTableProps = {
- heading: 'Docker Images',
+const DEFAULT_DOCKER_IMAGES_TABLE_PROPS: DockerTagsTableProps = {
+ heading: 'Docker Tags',
columns: ['name', 'username', 'status', 'architecture'],
initialPage: 0,
pageSize: 5,
@@ -122,10 +116,10 @@ const DEFAULT_DOCKER_IMAGES_TABLE_PROPS: DockerImagesTableProps = {
showCountInHeading: true
}
-export const DockerImagesTable = (props: Partial) => {
+export const DockerTagsTable = (props: Partial) => {
const { entity } = useEntity();
- const options: DockerImagesTableProps = {
+ const options: DockerTagsTableProps = {
...DEFAULT_DOCKER_IMAGES_TABLE_PROPS,
...props,
}
@@ -136,10 +130,16 @@ export const DockerImagesTable = (props: Partial) => {
/>);
}
+
const dockerApi = useApi(dockerApiRef);
const [containersCount, setContainersCount] = useState(0);
const columns = useMemo(() => getColumns(options), []);
+ const [error, setError] = useState< { message: string, name: string } | null>(null);
+
+ if (error) {
+ return ;
+ }
return (
) => {
pageSize: options.pageSize,
pageSizeOptions: options.pageSizeOptions
}}
+ emptyContent={
+
+ No Git Tags found
+
+ }
title={
(
@@ -164,17 +169,28 @@ export const DockerImagesTable = (props: Partial) => {
return dockerApi.getRepositories(`/docker/v2/namespaces/${url.organization}/repositories/${url.repository}/tags`, (query.page + 1), query.pageSize)
.then((res) => {
+ console.log('RES', res);
setContainersCount(res.count);
return {
data: res.results,
totalCount: res.count,
page: query.page
}
+ }).catch((err: any) => {
+ setError({
+ message: err.message,
+ name: err.status
+ });
+ return Promise.resolve({
+ data: [],
+ page: 0,
+ totalCount: 0
+ })
});
}
return Promise.resolve({
data: [],
- page: 1,
+ page: 0,
totalCount: 0
})
}}
diff --git a/src/components/index.ts b/src/components/index.ts
index 0848b7e..7a93de7 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -1 +1 @@
-export * from './DockerComponent/DockerComponent';
+export * from './Docker/DockerTagsTable';
diff --git a/src/index.ts b/src/index.ts
index df16408..143642a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,14 @@
+/**
+ * Export all the components
+ */
export * from './components';
+
+/**
+ * Export all the apis
+ */
export * from './apis';
+/**
+ * Export the plugin
+ */
export * from './plugin';
diff --git a/src/plugin.test.ts b/src/plugin.test.ts
index 14faf2c..b316bed 100644
--- a/src/plugin.test.ts
+++ b/src/plugin.test.ts
@@ -1,7 +1,13 @@
-import { dockerPlugin } from './plugin';
+import { DockerClient } from './apis';
+import { dockerTagsPlugin } from './plugin';
describe('docker', () => {
it('should export plugin', () => {
- expect(dockerPlugin).toBeDefined();
+ expect(dockerTagsPlugin).toBeDefined();
+ });
+ it('Should have the docker API', () => {
+ const apiFactories = Array.from(dockerTagsPlugin.getApis());
+ expect(apiFactories.length).toBe(1);
+ expect(apiFactories[0].factory({})).toBeInstanceOf(DockerClient);
});
});
diff --git a/src/plugin.ts b/src/plugin.ts
index f0a34f8..9002371 100644
--- a/src/plugin.ts
+++ b/src/plugin.ts
@@ -6,10 +6,11 @@ import {
discoveryApiRef,
fetchApiRef,
} from '@backstage/core-plugin-api';
+
import { dockerApiRef, DockerClient } from './apis';
-export const dockerPlugin = createPlugin({
- id: 'docker',
+export const dockerTagsPlugin = createPlugin({
+ id: 'docker.tags',
apis: [
createApiFactory({
api: dockerApiRef,
@@ -25,13 +26,13 @@ export const dockerPlugin = createPlugin({
],
});
-export const DockerRepositoriesWidget = dockerPlugin.provide(
+export const DockerTagsTableWidget = dockerTagsPlugin.provide(
createComponentExtension({
- name: 'DockerRepositoriesWidget',
+ name: 'DockerTagsTable',
component: {
lazy: () =>
- import('./components/DockerComponent/DockerComponent').then(
- d => d.DockerImagesTable,
+ import('./components/Docker/DockerTagsTable').then(
+ d => d.DockerTagsTable,
),
},
}),