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, ), }, }),