diff --git a/README.md b/README.md index 3f7ada6..3cee1ab 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ # Backstage JFrog artifactory libraries plugin - -The Backstage `jfrog-artifactory-libs` frontend plugin provides a simple way to display generated artifact (library) -details like - group, artifact, repository, what is the latest version, and it simply allows to copy library definition -for the package managers. -Nowadays, the plugin supports these package managers types in JFrog: Maven, Gradle, Sbt, Pypi, Docker. + The Backstage `jfrog-artifactory-libs` frontend plugin uses the JFrog Artifactory APIs to fetch details about artifacts (libraries) + and displays these details in the Backstage app, allowing users to view information about the artifact, such as the group, + artifact, repository, the latest version, and it also allows to copy library definition for the package managers. The plugin + supports these package managers types in JFrog: Maven, Gradle, Sbt, Pypi, Docker, NPM, Yarn, Nuget. ![Demo](./doc/artifact.gif) @@ -11,10 +10,11 @@ Nowadays, the plugin supports these package managers types in JFrog: Maven, Grad - [Backstage jfrog-artifactory-libs plugin](#backstage-xkcd-plugin) + - [Authentication](#authentication) - [Installation](#installation) - [Integration](#integration) - [Configuration](#configuration) - - [Support for scaffolding](#support-for-scaffolding) + - [Support for scaffolding](#support-for-scaffolding) - [How it works](#how-it-works) - [Changes](#changes) - [Contributing](#contributing) @@ -23,6 +23,12 @@ Nowadays, the plugin supports these package managers types in JFrog: Maven, Grad - [License](#license) +## Authentication: + +- The jfrog-artifactory-libs plugin does not directly handle authentication. Instead, it relies on [Backstage basic HTTP proxy package](https://backstage.io/docs/plugins/proxying/) + which is configured to forward requests to JFrog Artifactory instance. + + ## Installation To install the plugin, you'll need to add it to your Backstage app's dependencies using either Yarn or NPM. @@ -38,26 +44,32 @@ yarn add --cwd packages/app backstage-plugin-jfrog-artifactory-libs Once you've installed the plugin, you'll need to integrate it into your Backstage app. To do so, you'll need to following code into your BS instance: ### JFrogLibArtifactCard + Add the JFrogLibArtifactCard component to the `EntityPage.tsx` in your app: ```typescript jsx -import {JFrogLibArtifactCard, isJfrogArtifactAvailable} from 'backstage-plugin-jfrog-artifactory-libs'; +import { + JFrogLibArtifactCard, + isJfrogArtifactAvailable, +} from 'backstage-plugin-jfrog-artifactory-libs'; //.... const overviewContent = ( -// ... - - //... - - - - - - //... - - // ... + // ... + + //... + + + + + + //... + + // ... ); ``` + ### JFrogLibVerPage + If you want to browse libraries you can enable this component in your `App.tsx` file. It shows all component entities containing `jfrog.com/artifactory-artifact` attribute. ![Demo](./doc/libs.gif) @@ -66,8 +78,10 @@ If you want to browse libraries you can enable this component in your `App.tsx` const routes = ( //.... - } />}> - + } />} + > //.... ); @@ -75,7 +89,9 @@ const routes = ( ### Explore page - JFrogLibVerPageContent + This is a subcomponent of `JFrogLibVerPage` component. It's possible to integrate it for instance into your Explore page. + ```typescript jsx import { JFrogLibVerPageContent } from 'backstage-plugin-jfrog-artifactory-libs'; //.... @@ -100,13 +116,13 @@ import { JFrogLibVerPageContent } from 'backstage-plugin-jfrog-artifactory-libs' Set up a proxy for the JFrog API by adding the following configuration to your `app-config.yaml` file: ```yaml - '/artifactory-proxy/': - target: 'https://your-jfrog-artifactory-instance.com' - headers: - # if you use Jfrog instance for anonymous user token is not required, but it is also required for Docker package type - Authorization: Bearer ${ARTIFACTORY_TOKEN} - X-Result-Detail: 'properties' - Accept: '*' +'/artifactory-proxy/': + target: 'https://your-jfrog-artifactory-instance.com' + headers: + # if you use Jfrog instance for anonymous user token is not required, but it is also required for Docker package type + Authorization: Bearer ${ARTIFACTORY_TOKEN} + X-Result-Detail: 'properties' + Accept: '*' ``` You have to also reference your artifactory URL (used for UI browse links) and your proxy configuration. @@ -115,7 +131,7 @@ You have to also reference your artifactory URL (used for UI browse links) and y jfrog: artifactory: url: 'https://your-jfrog-artifactory-instance.com' - proxyPath: '/artifactory-proxy/' # optional, /artifactory-proxy/ is default value + proxyPath: '/artifactory-proxy/' # optional, /artifactory-proxy/ is default value ``` ### Catalog-info.yaml @@ -123,30 +139,34 @@ jfrog: Artifact details are correlated to Backstage entities using an annotation added in the entity's catalog-info.yaml file. ```yaml - metadata: - annotations: - # -- required values -- - jfrog.com/artifactory-artifact: 'artifact-name' - jfrog.com/artifactory-repo: 'maven-local' - - jfrog.com/artifactory-group: 'com.mycompany' # optional string - can be blank for pypi, necessary for Maven repos - - # -- optional values -- - jfrog.com/artifactory-scope: 'compile' # optional string, one of these [compile, test,provided,runtime,classpath,optional] - jfrog.com/artifactory-packaging: 'aar' #optional string, eg. `aar` - +#... +metadata: + annotations: + # -- required values -- + jfrog.com/artifactory-artifact: 'artifact-name' + jfrog.com/artifactory-repo: 'maven-local' + + jfrog.com/artifactory-group: 'com.mycompany' # optional string - can be blank for pypi, necessary for Maven repos + + # -- optional values -- + jfrog.com/artifactory-scope: 'compile' # optional string, one of these [compile, test,provided,runtime,classpath,optional] + jfrog.com/artifactory-packaging: 'aar' #optional string, eg. `aar` +#... ``` And that's it! The plugin should now be integrated into your Backstage app, and you should see the Artifact card when you navigate to the entity page where it's included. For a docker image you define repository and artifact name. Both formats are supported: + ```yaml - metadata: - annotations: - # -- required values -- - jfrog.com/artifactory-artifact: 'docker.mydomain.com/mygroup/my/artifact-name' # or simply 'mygroup/my/artifact-name' - jfrog.com/artifactory-repo: 'docker-local' +#... +metadata: + annotations: + # -- required values -- + jfrog.com/artifactory-artifact: 'docker.mydomain.com/mygroup/my/artifact-name' # or simply 'mygroup/my/artifact-name' + jfrog.com/artifactory-repo: 'docker-local' +#... ``` ![Demo](./doc/dockerfile.gif) @@ -163,28 +183,33 @@ LibArtifactCard.defaultProps = { showMaven: true, // whether to show Maven package manager tab showSbt: true, // whether to show Sbt package manager tab showPip: true, // whether to show Pip package manager tab + showYarn: true, // whether to show Yarn package manager tab showDockerfile: true, // whether to show Dockerfile tab // it hides Maven and Gradle tabs if the current repository package type is `PyPi` autohideTabs: true, - showBrowseRepositoryLink: true // whether to show Browse to URL deep link under bottom of the Card + showBrowseRepositoryLink: true, // whether to show Browse to URL deep link under bottom of the Card // which link to open browseLink: browseLinkDefault, }; ``` + ### Support for scaffolding + In [this document](./doc/SCAFFOLDING.md) you can find detailed information how to integrate this plugin into scaffolding templates. -It also adds a new extension UI component called `ArtifactRepositoryPicker` for interactive repository selection. +It also adds a new extension UI component called `ArtifactRepositoryPicker` for interactive repository selection. ## How it works Plugin uses JFrog APIs to find latest version. It's necessary to specify `ARTIFACTORY_TOKEN` in the `app-config.yaml` file if you don't allow to access API for anonymous user. -## Changes -Version 1.0.9 - - JFrogLibVerPageContent and JFrogLibVerPage components added - - (!) renamed LibArtifactCard into JFrogLibArtifactCard +## Changes + +Version 1.0.9 + +- JFrogLibVerPageContent and JFrogLibVerPage components added +- (!) renamed LibArtifactCard into JFrogLibArtifactCard ## Contributing diff --git a/package.json b/package.json index 2ed7441..64a46c2 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", @@ -31,6 +30,7 @@ "src/components/types.ts" ] }, + "sideEffects": false, "scripts": { "start": "backstage-cli package start", "dev": "concurrently \"yarn start\" \"yarn start-backend\"", @@ -49,40 +49,42 @@ "bump": "yarn run standard-version --no-verify --releaseCommitMessageFormat 'v{{currentTag}}'" }, "dependencies": { - "@backstage/core-components": "^0.13.4", - "@backstage/plugin-catalog-react": "^1.8.3", - "@backstage/catalog-model": "^1.4.1", - "@backstage/core-plugin-api": "^1.5.3", - "@backstage/plugin-catalog": "^1.12.4", - "@backstage/plugin-scaffolder": "^1.14.4", - "@backstage/plugin-scaffolder-react": "^1.6.1", - "@backstage/theme": "^0.4.1", + "@backstage/catalog-model": "^1.7.0", + "@backstage/config": "^1.2.0", + "@backstage/core-components": "^0.15.0", + "@backstage/core-plugin-api": "^1.9.4", + "@backstage/plugin-catalog": "^1.23.1", + "@backstage/plugin-catalog-react": "^1.13.1", + "@backstage/plugin-scaffolder": "^1.25.1", + "@backstage/plugin-scaffolder-react": "^1.12.1", + "@backstage/types": "^1.1.1", "@material-ui/core": "^4.9.13", - "@material-ui/icons": "^4.9.1", - "@material-ui/lab": "^4.0.0-alpha.60", + "@material-ui/lab": "^4.0.0-alpha.61", "@types/pluralize": "^0.0.33", "lodash": "^4.17.21", - "react-use": "^17.2.4" + "pluralize": "^8.0.0", + "react-use": "^17.2.4", + "zod": "^3.23.8" }, "peerDependencies": { - "react": "^16.13.1 || ^17.2.0" + "react": "^16.13.1 || ^17.2.0", + "react-router": "6.0.0-beta.0 || ^6.3.0" }, "devDependencies": { - "@backstage/cli": "^0.22.12", - "@backstage/core-app-api": "^1.9.1", - "@backstage/dev-utils": "^1.0.20", - "@backstage/test-utils": "^1.4.2", + "@backstage/cli": "^0.27.1", + "@backstage/core-app-api": "^1.15.0", + "@backstage/dev-utils": "^1.1.1", + "@backstage/test-utils": "^1.6.0", "@spotify/prettier-config": "^14.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", + "@types/lodash": "^4.14.202", "@types/node": "*", + "@types/pluralize": "^0.0.33", "cross-fetch": "^3.1.5", "msw": "^1.0.0", - "@types/pluralize": "^0.0.33", - "@types/lodash": "^4.14.202", "prettier": "^2.2.1", - "react-router": "6.0.0-beta.0", "standard-version": "^9.5.0" }, "prettier": "@spotify/prettier-config", diff --git a/src/components/ArtifactRepositoryPicker/index.ts b/src/components/ArtifactRepositoryPicker/index.ts index d23e11b..de2facf 100644 --- a/src/components/ArtifactRepositoryPicker/index.ts +++ b/src/components/ArtifactRepositoryPicker/index.ts @@ -18,5 +18,5 @@ export { type RepositoryPickerUiOptions, } from './schema'; export { ArtifactRepositoryPicker } from './ArtifactRepositoryPicker'; -export type * from './ArtifactRepositoryPicker' -export type * from './schema' \ No newline at end of file +export type * from './ArtifactRepositoryPicker'; +export type * from './schema'; diff --git a/src/components/ArtifactRepositoryPicker/schema.ts b/src/components/ArtifactRepositoryPicker/schema.ts index 6ce0f05..690be8d 100644 --- a/src/components/ArtifactRepositoryPicker/schema.ts +++ b/src/components/ArtifactRepositoryPicker/schema.ts @@ -14,7 +14,8 @@ * limitations under the License. */ import { z } from 'zod'; -import {CustomFieldExtensionSchema, makeFieldSchemaFromZod} from '@backstage/plugin-scaffolder'; +import { makeFieldSchemaFromZod } from '@backstage/plugin-scaffolder'; +import { CustomFieldExtensionSchema } from '@backstage/plugin-scaffolder-react'; /** * @public diff --git a/src/components/LibArtifactCard/LibArtifactCard.tsx b/src/components/LibArtifactCard/LibArtifactCard.tsx index 40e00d4..1102138 100644 --- a/src/components/LibArtifactCard/LibArtifactCard.tsx +++ b/src/components/LibArtifactCard/LibArtifactCard.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { configApiRef, identityApiRef, useApi } from '@backstage/core-plugin-api'; +import { configApiRef, fetchApiRef, useApi } from '@backstage/core-plugin-api'; import { ResponseErrorPanel } from '@backstage/core-components'; import { useEntity } from '@backstage/plugin-catalog-react'; import { LibArtifactCardProps } from '../../types'; @@ -13,8 +13,8 @@ import { ArtifactInfo } from './api'; export const DEFAULT_PROXY_PATH = '/artifactory-proxy/'; export const LibArtifactCard = (props: LibArtifactCardProps) => { const config = useApi(configApiRef); + const fetchApi = useApi(fetchApiRef); const { entity } = useEntity(); - const identity = useApi (identityApiRef); const artifactoryUrl = config.getString('jfrog.artifactory.url'); @@ -22,7 +22,7 @@ export const LibArtifactCard = (props: LibArtifactCardProps) => { const { value, loading, error } = useAsync(async () => { try { - return await libraryInfo(entity, config, identity); + return await libraryInfo(entity, config, fetchApi); } catch (e) { if (!(e instanceof Error)) { throw new Error(e as string); @@ -65,6 +65,9 @@ LibArtifactCard.defaultProps = { showMaven: true, // whether to show Maven package manager tab showSbt: true, // whether to show Sbt package manager tab showPip: true, // whether to show Pip package manager tab + showNpm: true, // whether to show Npm package manager tab + showYarn: true, // whether to show Npm package manager tab + showNuget: true, // whether to show Nuget package manager tab showDockerfile: true, // whether to show Dockerfile tab // it hides Maven and Gradle tabs if the current repository package type is `PyPi` autohideTabs: true, diff --git a/src/components/LibArtifactCard/api.ts b/src/components/LibArtifactCard/api.ts index 47add76..e84b154 100644 --- a/src/components/LibArtifactCard/api.ts +++ b/src/components/LibArtifactCard/api.ts @@ -1,13 +1,15 @@ -// import { on } from 'events'; import { LibraryArtifact, MetadataResponse } from '../../types'; import { findLatestVersion } from './versionUtils'; -import { IdentityApi } from '@backstage/core-plugin-api'; +import { FetchApi } from '@backstage/core-plugin-api'; export type GeneratedCode = { gradle: string; maven: string; sbt: string; pip: string; + nuget: string; + npm: string; + yarn: string; }; export type ArtifactInfo = { @@ -47,23 +49,13 @@ export async function getErrorMessage(response: Response) { } export async function getRepositoryType( - fetch: { - (input: RequestInfo | URL, init?: RequestInit): Promise; - (input: RequestInfo | URL, init?: RequestInit): Promise; - }, url: string, { repo }: LibraryArtifact, - identityApi: IdentityApi + fetchApi: FetchApi, ) { - // Obtain the token - - const { token: idToken } = await identityApi.getCredentials(); - - const response = await fetch(`${url}artifactory/api/repositories/${repo}`, { - headers: { - Authorization: `Bearer ${idToken}`, - }, - }); + const response = await fetchApi.fetch( + `${url}artifactory/api/repositories/${repo}`, + ); if (response.status === 404) { throw new Error(`Repository ${repo} was not found`); } else { @@ -80,22 +72,12 @@ export async function getRepositoryType( } export async function getMavenLatestVersion( - fetch: { - (input: RequestInfo | URL, init?: RequestInit): Promise; - (input: RequestInfo | URL, init?: RequestInit): Promise; - }, url: string, { group, artifact, repo }: LibraryArtifact, - identityApi: IdentityApi, + fetchApi: FetchApi, ) { - - const { token: idToken } = await identityApi.getCredentials(); - - const response = await fetch( - `${url}artifactory/api/search/latestVersion?g=${group}&a=${artifact}&repos=${repo}`, { - headers: { - Authorization: `Bearer ${idToken}`, - },} + const response = await fetchApi.fetch( + `${url}artifactory/api/search/latestVersion?g=${group}&a=${artifact}&repos=${repo}`, ); if (response.status === 404) { return undefined; @@ -109,13 +91,13 @@ export async function getMavenLatestVersion( } } -export function getMetadataVersionQuery(artifact: string) { +export function getMetadataVersionQuery(artifact: string, packageType: string) { const query = { query: 'query ($filter: VersionFilter!, $first: Int, $orderBy: VersionOrder) { versions (filter: $filter, first: $first, orderBy: $orderBy) { edges { node { name, created, modified, package { id }, repos { name, type, leadFilePath }, licenses { name, source }, size, stats { downloadCount }, vulnerabilities { critical, high, medium, low, info, unknown, skipped }, files { name, lead, size, md5, sha1, sha256, mimeType } } } } }', variables: { filter: { - packageId: `docker://${artifact}`, + packageId: `${packageType}://${artifact}`, name: '*', ignorePreRelease: false, }, @@ -148,24 +130,29 @@ export function extractArtifactFromFullDockerName(artifact: string): string { return artifactWithoutVersion; } -export async function getDockerLatestVersion( - fetch: { - (input: RequestInfo | URL, init?: RequestInit): Promise; - (input: RequestInfo | URL, init?: RequestInit): Promise; - }, +function extractPathFromLeadFilePath(path: string) { + if (path) { + const lastSlash = path.lastIndexOf('/'); + if (lastSlash) { + return path.substring(0, lastSlash); + } + } + return undefined; +} + +export async function getLatestCreatedVersion( url: string, - { artifact }: LibraryArtifact, - identityApi: IdentityApi, + libraryArtifact: LibraryArtifact, + packageType: string, + fetchApi: FetchApi, + artifactName: (libraryArtifact: LibraryArtifact) => string, ) { - const { token: idToken } = await identityApi.getCredentials(); - - const response = await fetch(`${url}/metadata/api/v1/query`, { + const response = await fetchApi.fetch(`${url}metadata/api/v1/query`, { headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${idToken}`, }, method: 'POST', - body: getMetadataVersionQuery(extractArtifactFromFullDockerName(artifact)), + body: getMetadataVersionQuery(artifactName(libraryArtifact), packageType), }); if (response.status === 404) { return undefined; @@ -182,6 +169,10 @@ export async function getDockerLatestVersion( version: node.name, size: Number(node.size), statsDownload: node.stats.downloadCount, + filePath: + node.repos && node.repos.length > 0 + ? extractPathFromLeadFilePath(node.repos[0].leadFilePath) + : undefined, lastModified: node.modified ? new Date(node.modified) : undefined, }; } @@ -190,21 +181,12 @@ export async function getDockerLatestVersion( } export async function getPypiLatestVersion( - fetch: { - (input: RequestInfo | URL, init?: RequestInit): Promise; - (input: RequestInfo | URL, init?: RequestInit): Promise; - }, url: string, { artifact, repo }: LibraryArtifact, - identityApi: IdentityApi, + fetchApi: FetchApi, ) { - const { token: idToken } = await identityApi.getCredentials(); - - const response = await fetch( - `${url}/artifactory/api/search/prop?pypi.name=${artifact}&repos=${repo}`, { - headers: { - Authorization: `Bearer ${idToken}`, - },} + const response = await fetchApi.fetch( + `${url}artifactory/api/search/prop?pypi.name=${artifact}&repos=${repo}`, ); if (response.status === 404) { return undefined; diff --git a/src/components/LibArtifactCard/codeSnippets.ts b/src/components/LibArtifactCard/codeSnippets.ts index f6c7b57..e6fc9fe 100644 --- a/src/components/LibArtifactCard/codeSnippets.ts +++ b/src/components/LibArtifactCard/codeSnippets.ts @@ -1,16 +1,14 @@ import { LibraryArtifact } from '../../types'; -import {GeneratedCode} from "./api"; +import { GeneratedCode } from './api'; function removeScalaVersion(artifactName: string) { const underscorePos = artifactName.lastIndexOf('_'); - if (underscorePos > 0) { - if ( - artifactName.substring(underscorePos + 1).match(/^[0-9]|\?/gi) != null - ) { - return artifactName.substring(0, underscorePos); - } - } - return artifactName; + const hasScalaVersion = + underscorePos > 0 && + artifactName.substring(underscorePos + 1).match(/^[0-9]|\?/gi); + return hasScalaVersion + ? artifactName.substring(0, underscorePos) + : artifactName; } function generateGradleMavenCode( @@ -33,40 +31,40 @@ function generateGradleMavenCode( if (classifier !== '') { mavenClassifier = `\n\t${classifier}`; - gradleClassifier = ':' + classifier; - sbtClassifier = ' classifier ' + '"' + classifier + '"'; + gradleClassifier = `:${classifier}`; + sbtClassifier = ` classifier ` + `"${classifier}"`; } const version = lib.version !== undefined ? lib.version : ''; - const gradle = - gradleScope + - func1 + - lib.group + - ':' + - artifactWithoutScalaVersion + - gradleScalaVersion + - ':' + - version + - gradleClassifier + - packaging + - func2 + - transitive + - '\n'; + const gradle = `${ + gradleScope + func1 + lib.group + }:${artifactWithoutScalaVersion}${gradleScalaVersion}:${version}${gradleClassifier}${packaging}${func2}${transitive}\n`; const maven = `\n\t${lib.group}\n\t${lib.artifact}\n\t${version}${mavenClassifier}\n\t${mavenScope}\n\n`; const sbt = `"${lib.group}" ${sbtScalaVersion}% "${artifactWithoutScalaVersion}" % "${version}"${sbtClassifier}${sbtScope}` + ',\n'; - return { gradle: gradle, maven: maven, sbt: sbt }; + const npm = `"${lib.artifact}": "^${version}"`; + // command to add the package to the project using yarn + const yarn = `yarn add ${lib.artifact}`; + const nuget = ``; + return { + gradle: gradle, + maven: maven, + sbt: sbt, + npm: npm, + nuget: nuget, + yarn: yarn, + }; } function getScalaVersion(artifactName: string) { const underscorePos = artifactName.lastIndexOf('_'); if (underscorePos > 0) { const classifier = artifactName.substring(underscorePos + 1); - if (classifier.match(/^[0-9]|\?/gi) != null) { + if (classifier.match(/^[0-9]|\?/gi)) { return classifier; } } @@ -82,6 +80,9 @@ export function generatePackageManagersCode( let maven = ''; let sbt = ''; let pip = ''; + let nuget = ''; + let npm = ''; + let yarn = ''; const version = lib.version !== undefined ? lib.version : ''; @@ -92,9 +93,9 @@ export function generatePackageManagersCode( if (replaceScalaVersion && scalaVersion) { scalaVersion = '?'; } - let gradleScalaVersion = scalaVersion ? '_' + scalaVersion : ''; - let sbtScalaVersion = scalaVersion ? '%' : ''; - let artifactWithoutScalaVersion = removeScalaVersion(lib.artifact); + const gradleScalaVersion = scalaVersion ? `_${scalaVersion}` : ''; + const sbtScalaVersion = scalaVersion ? '%' : ''; + const artifactWithoutScalaVersion = removeScalaVersion(lib.artifact); let gradleScope = 'implementation'; let mavenScope = 'compile'; @@ -131,7 +132,7 @@ export function generatePackageManagersCode( } let packaging = ''; if (lib.packaging) { - packaging = '@' + lib.packaging; + packaging = `@${lib.packaging}`; } if ( lib.classifiers && @@ -156,6 +157,8 @@ export function generatePackageManagersCode( gradle += __ret.gradle; maven += __ret.maven; sbt += __ret.sbt; + nuget += __ret.nuget; + npm += __ret.npm; }); } else { const __ret = generateGradleMavenCode( @@ -175,11 +178,17 @@ export function generatePackageManagersCode( gradle += __ret.gradle; maven += __ret.maven; sbt += __ret.sbt; + nuget += __ret.nuget; + npm += __ret.npm; + yarn += __ret.yarn; } return { - gradle: gradle, - maven: maven, - sbt: sbt, - pip: pip, + gradle, + maven, + sbt, + pip, + nuget, + npm, + yarn, }; } diff --git a/src/components/LibArtifactCard/libraryInfo.tsx b/src/components/LibArtifactCard/libraryInfo.tsx index 9f9a509..8692adf 100644 --- a/src/components/LibArtifactCard/libraryInfo.tsx +++ b/src/components/LibArtifactCard/libraryInfo.tsx @@ -1,26 +1,27 @@ import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; - -import { IdentityApi } from '@backstage/core-plugin-api'; - import { ENTITY_ARTIFACT, ENTITY_GROUP, ENTITY_PACKAGING, ENTITY_REPO, ENTITY_SCOPE, - isJfrogArtifactAvailable, isJfrogRepoAvailable, - LibraryArtifact -} from "../../types"; + isJfrogArtifactAvailable, + isJfrogRepoAvailable, + LibraryArtifact, +} from '../../types'; import { ArtifactInfo, extractArtifactFromFullDockerName, - getDockerLatestVersion, getMavenLatestVersion, getPypiLatestVersion, + getLatestCreatedVersion, + getMavenLatestVersion, + getPypiLatestVersion, getRepositoryType, - removeDockerVersion -} from "./api"; -import {generatePackageManagersCode} from "./codeSnippets"; + removeDockerVersion, +} from './api'; +import { generatePackageManagersCode } from './codeSnippets'; +import { FetchApi } from '@backstage/core-plugin-api'; export const DEFAULT_PROXY_PATH = '/artifactory-proxy/'; @@ -46,7 +47,7 @@ export function checkAnnotationsPresent(entity: Entity) { export async function libraryInfo( entity: Entity, config: Config, - identity: IdentityApi, + fetchApi: FetchApi, ): Promise { const artifactoryBackendProxy = config.getOptionalString('jfrog.artifactory.proxyPath') || @@ -57,31 +58,57 @@ export async function libraryInfo( const proxyUrl = `/proxy${artifactoryBackendProxy}`; // try { const url = `${backendUrl}/api${proxyUrl}`; - const { packageType } = await getRepositoryType(fetch, url, entityArtifact,identity); + + const { packageType } = await getRepositoryType( + url, + entityArtifact, + fetchApi, + ); let version; const artInfo = { ...entityArtifact }; switch (packageType) { + case 'maven': + version = await getMavenLatestVersion(url, entityArtifact, fetchApi); + break; case 'docker': - const dockerInfo = await getDockerLatestVersion( - fetch, + // eslint-disable-next-line no-case-declarations -- this just should be rewritten properly + const info = await getLatestCreatedVersion( url, entityArtifact, - identity, + packageType, + fetchApi, + libArt => extractArtifactFromFullDockerName(libArt.artifact), ); - artInfo.stats = dockerInfo?.statsDownload; - artInfo.size = dockerInfo?.size; + artInfo.stats = info?.statsDownload; + artInfo.size = info?.size; artInfo.artifactFullName = removeDockerVersion(artInfo.artifact); - artInfo.lastModified = dockerInfo?.lastModified - ? new Date(dockerInfo?.lastModified) + artInfo.lastModified = info?.lastModified + ? new Date(info?.lastModified) : undefined; artInfo.artifact = extractArtifactFromFullDockerName(artInfo.artifact); - version = dockerInfo?.version; + version = info?.version; break; case 'pypi': - version = await getPypiLatestVersion(fetch, url, entityArtifact,identity); + version = await getPypiLatestVersion(url, entityArtifact, fetchApi); + break; + default: { + const info = await getLatestCreatedVersion( + url, + entityArtifact, + packageType, + fetchApi, + libArt => libArt.artifact, + ); + artInfo.stats = info?.statsDownload; + artInfo.size = info?.size; + artInfo.artifactFullName = artInfo.artifact; + artInfo.filePath = info?.filePath; + artInfo.lastModified = info?.lastModified + ? new Date(info?.lastModified) + : undefined; + version = info?.version; break; - default: - version = await getMavenLatestVersion(fetch, url, entityArtifact,identity); + } } artInfo.version = version; @@ -99,4 +126,4 @@ export async function libraryInfo( // } // return ''; // } -} \ No newline at end of file +} diff --git a/src/components/LibArtifactCard/utils.ts b/src/components/LibArtifactCard/utils.ts index 5314f43..5b917ef 100644 --- a/src/components/LibArtifactCard/utils.ts +++ b/src/components/LibArtifactCard/utils.ts @@ -2,6 +2,7 @@ export function formatDate(date: Date): string { if (!date) { return 'N/A'; } + // eslint-disable-next-line new-cap const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; const options: Intl.DateTimeFormatOptions = { timeZone, diff --git a/src/components/LibArtifactCard/versionUtils.ts b/src/components/LibArtifactCard/versionUtils.ts index d777d27..513997b 100644 --- a/src/components/LibArtifactCard/versionUtils.ts +++ b/src/components/LibArtifactCard/versionUtils.ts @@ -1,5 +1,5 @@ function convertToNumberOrString(value: string): number | string { - const parsedNumber = parseInt(value); + const parsedNumber = parseInt(value, 10); return isNaN(parsedNumber) ? value : parsedNumber; } diff --git a/src/components/LibVerTabbedContent/LibVerTabbedContent.tsx b/src/components/LibVerTabbedContent/LibVerTabbedContent.tsx index 698adc5..cac4e43 100644 --- a/src/components/LibVerTabbedContent/LibVerTabbedContent.tsx +++ b/src/components/LibVerTabbedContent/LibVerTabbedContent.tsx @@ -7,9 +7,10 @@ import { } from '@backstage/core-components'; import { LibVerView } from '../LibVerView'; import React from 'react'; -import { ArtifactInfo } from '../LibArtifactCard/api'; +import { ArtifactInfo, GeneratedCode } from '../LibArtifactCard/api'; import { Divider, makeStyles } from '@material-ui/core'; import { ClassNameMap } from '@material-ui/core/styles/withStyles'; +import { capitalize } from 'lodash'; const useStyles = makeStyles({ dividerMargin: { @@ -39,12 +40,17 @@ function isPackageType( ); } -function getPipTab(artifactInfo: ArtifactInfo[] | undefined) { +function getTab( + type: keyof GeneratedCode, + language: string = 'plainText', + artifactInfo: ArtifactInfo[] | undefined, + tabTitle?: string, +) { return ( - + item.code().pip).join('\n') || ''} + language={language} + text={artifactInfo?.map(item => item.code()[type]).join('\n') || ''} showCopyCodeButton /> @@ -73,42 +79,6 @@ function getDockerfileTab(artifactInfo: ArtifactInfo[] | undefined) { ); } -function getSbtTab(artifactInfo: ArtifactInfo[] | undefined) { - return ( - - item.code().sbt).join('\n') || ''} - showCopyCodeButton - /> - - ); -} - -function getMavenTab(artifactInfo: ArtifactInfo[] | undefined) { - return ( - - ); -} - -function getGradleTab(artifactInfo: ArtifactInfo[] | undefined) { - return ( - - ); -} - export function infoTab( loading: boolean, artifactInfo: ArtifactInfo[], @@ -153,16 +123,25 @@ export const LibVerTabbedContent = ({ } if (!loading) { if (props.showGradle && isPackageType(props, artifactInfo, 'maven')) { - tabs.push(getGradleTab(artifactInfo)); + tabs.push(getTab('gradle', 'groovy', artifactInfo)); } if (props.showMaven && isPackageType(props, artifactInfo, 'maven')) { - tabs.push(getMavenTab(artifactInfo)); + tabs.push(getTab('maven', 'xml', artifactInfo)); } if (props.showSbt && isPackageType(props, artifactInfo, 'maven')) { - tabs.push(getSbtTab(artifactInfo)); + tabs.push(getTab('sbt', 'plainText', artifactInfo)); } if (props.showPip && isPackageType(props, artifactInfo, 'pypi')) { - tabs.push(getPipTab(artifactInfo)); + tabs.push(getTab('pip', 'plainText', artifactInfo)); + } + if (props.showNuget && isPackageType(props, artifactInfo, 'nuget')) { + tabs.push(getTab('nuget', 'xml', artifactInfo)); + } + if (props.showNpm && isPackageType(props, artifactInfo, 'npm')) { + tabs.push(getTab('npm', 'json', artifactInfo)); + } + if (props.showYarn && isPackageType(props, artifactInfo, 'npm')) { + tabs.push(getTab('yarn', 'plainText', artifactInfo)); } if (props.showDockerfile && isPackageType(props, artifactInfo, 'docker')) { tabs.push(getDockerfileTab(artifactInfo)); diff --git a/src/components/LibVerView/LibVerView.tsx b/src/components/LibVerView/LibVerView.tsx index e7fb014..cc1075b 100644 --- a/src/components/LibVerView/LibVerView.tsx +++ b/src/components/LibVerView/LibVerView.tsx @@ -17,15 +17,27 @@ function getBrowseRepoUrl(artifactoryUrl: string, lib: LibraryArtifact) { } function getBrowsePackageUrl(artifactoryUrl: string, lib: LibraryArtifact) { - return `${getBrowseRepoUrl(artifactoryUrl, lib)}${ - lib.group ? `/${lib.group?.replaceAll('.', '/')}` : '' - }`; + let result = ''; + if (lib.filePath) { + result = `/${lib.filePath}`; + } else { + if (lib.group) { + if (!lib.group.startsWith('/')) { + result += '/'; + } + result += lib.group.replaceAll('.', '/'); + } + } + return `${getBrowseRepoUrl(artifactoryUrl, lib)}${result}`; } export function getBrowseArtifactUrl( artifactoryUrl: string, lib: LibraryArtifact, ) { + if (lib.filePath) { + return `${getBrowsePackageUrl(artifactoryUrl, lib)}`; + } return `${getBrowsePackageUrl(artifactoryUrl, lib)}/${lib.artifact}`; } @@ -33,6 +45,9 @@ export function getBrowserVersionUrl( artUrl: string, lib: LibraryArtifact, ): string { + if (lib.filePath) { + return `${getBrowsePackageUrl(artUrl, lib)}`; + } return `${getBrowseArtifactUrl(artUrl, lib)}/${ lib.version !== undefined ? lib.version : '' }`; diff --git a/src/components/LibverPage/EntityLibraryFilter.tsx b/src/components/LibverPage/EntityLibraryFilter.tsx index 41af2a4..4c5a8d0 100644 --- a/src/components/LibverPage/EntityLibraryFilter.tsx +++ b/src/components/LibverPage/EntityLibraryFilter.tsx @@ -8,28 +8,26 @@ import { ENTITY_ARTIFACT } from '../../types'; import { useEffect } from 'react'; class EntityLibraryFilter implements EntityFilter { - getCatalogFilters(): Record { - return { - [`metadata.annotations.${ENTITY_ARTIFACT}`]: CATALOG_FILTER_EXISTS, - }; - } + getCatalogFilters(): Record { + return { + [`metadata.annotations.${ENTITY_ARTIFACT}`]: CATALOG_FILTER_EXISTS, + }; + } } export type CustomFilters = DefaultEntityFilters & { - libraryFilters?: EntityLibraryFilter + libraryFilters?: EntityLibraryFilter; }; export const EntityLibraryFilterPicker = () => { - const { - updateFilters - } = useEntityList(); + const { updateFilters } = useEntityList(); - useEffect(() => { - const entityLibraryFilter = new EntityLibraryFilter(); - updateFilters({ - libraryFilters: entityLibraryFilter, - }); - }, [updateFilters]); + useEffect(() => { + const entityLibraryFilter = new EntityLibraryFilter(); + updateFilters({ + libraryFilters: entityLibraryFilter, + }); + }, [updateFilters]); - return null; -}; \ No newline at end of file + return null; +}; diff --git a/src/components/LibverPage/LibverPage.tsx b/src/components/LibverPage/LibverPage.tsx index bbfd88c..7161805 100644 --- a/src/components/LibverPage/LibverPage.tsx +++ b/src/components/LibverPage/LibverPage.tsx @@ -6,7 +6,7 @@ import { TableColumn, TableProps, } from '@backstage/core-components'; -import {configApiRef, useApi} from '@backstage/core-plugin-api'; +import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { CatalogFilterLayout, EntityKindPicker, @@ -15,10 +15,10 @@ import { EntityTagPicker, EntityTypePicker, } from '@backstage/plugin-catalog-react'; -import React, {ReactNode} from 'react'; -import {LibverTable} from '../LibverTable'; -import {EntityLibraryFilterPicker} from './EntityLibraryFilter'; -import {LibverTableRow} from '../../types'; +import React, { ReactNode } from 'react'; +import { LibverTable } from '../LibverTable'; +import { EntityLibraryFilterPicker } from './EntityLibraryFilter'; +import { LibverTableRow } from '../../types'; /** * Props for root catalog pages. @@ -44,7 +44,7 @@ export function LibverPageContent(props: LibverPageProps) { -