Commit 76b2fa1f authored by Imre Farkas's avatar Imre Farkas

Merge branch '7603-make-it-easy-to-generate-and-share-the-maven-xml-for-a-library' into 'master'

Add Maven commands to package detail page

Closes #7603

See merge request gitlab-org/gitlab!20300
parents c5ec3889 a5091785
---
title: Add Maven installation commands to package detail page for Maven packages
merge_request: 20300
author:
type: added
......@@ -10,7 +10,8 @@ import {
} from '@gitlab/ui';
import _ from 'underscore';
import PackageInformation from './information.vue';
import PackageInstallation from './installation.vue';
import NpmInstallation from './npm_installation.vue';
import MavenInstallation from './maven_installation.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
......@@ -28,7 +29,8 @@ export default {
GlTable,
Icon,
PackageInformation,
PackageInstallation,
NpmInstallation,
MavenInstallation,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -67,14 +69,24 @@ export default {
type: String,
required: true,
},
mavenPath: {
type: String,
required: true,
},
mavenHelpPath: {
type: String,
required: true,
},
},
computed: {
isNpmPackage() {
return this.packageEntity.package_type === PackageType.NPM;
},
isMavenPackage() {
return this.packageEntity.package_type === PackageType.MAVEN;
},
isValidPackage() {
if (this.packageEntity.name) {
return true;
}
return false;
return Boolean(this.packageEntity.name);
},
canDeletePackage() {
return this.canDelete && this.destroyPath;
......@@ -204,20 +216,31 @@ export default {
</div>
<div class="row prepend-top-default" data-qa-selector="package_information_content">
<package-information :type="packageEntity.package_type" :information="packageInformation" />
<package-information
v-if="packageMetadata"
:heading="packageMetadataTitle"
:information="packageMetadata"
:show-copy="true"
/>
<package-installation
v-else
:type="packageEntity.package_type"
:name="packageEntity.name"
:registry-url="npmPath"
:help-url="npmHelpPath"
/>
<div class="col-sm-6">
<package-information :information="packageInformation" />
<package-information
v-if="packageMetadata"
:heading="packageMetadataTitle"
:information="packageMetadata"
:show-copy="true"
/>
</div>
<div class="col-sm-6">
<npm-installation
v-if="isNpmPackage"
:name="packageEntity.name"
:registry-url="npmPath"
:help-url="npmHelpPath"
/>
<maven-installation
v-else-if="isMavenPackage"
:maven-metadata="packageEntity.maven_metadatum"
:registry-url="mavenPath"
:help-url="mavenHelpPath"
/>
</div>
</div>
<gl-table
......
......@@ -15,15 +15,24 @@ export default {
type: String,
required: true,
},
multiline: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<template>
<div class="input-group append-bottom-10">
<div v-if="!multiline" class="input-group append-bottom-10">
<input :value="instruction" type="text" class="form-control monospace" readonly />
<span class="input-group-append">
<clipboard-button :text="instruction" :title="copyText" class="input-group-text" />
</span>
</div>
<div v-else>
<pre>{{ instruction }}</pre>
</div>
</template>
......@@ -28,26 +28,24 @@ export default {
</script>
<template>
<div class="col-sm-6">
<div class="card">
<div class="card-header">
<strong>{{ heading }}</strong>
</div>
<ul class="content-list">
<li v-for="(item, index) in information" :key="index">
<span class="text-secondary">{{ item.label }}</span>
<div class="pull-right">
<span>{{ item.value }}</span>
<clipboard-button
v-if="showCopy"
:text="item.value"
:title="sprintf(__('Copy %{field}'), { field: item.label })"
css-class="border-0 text-secondary py-0"
/>
</div>
</li>
</ul>
<div class="card">
<div class="card-header">
<strong>{{ heading }}</strong>
</div>
<ul class="content-list">
<li v-for="(item, index) in information" :key="index">
<span class="text-secondary">{{ item.label }}</span>
<div class="pull-right">
<span>{{ item.value }}</span>
<clipboard-button
v-if="showCopy"
:text="item.value"
:title="sprintf(__('Copy %{field}'), { field: item.label })"
css-class="border-0 text-secondary py-0"
/>
</div>
</li>
</ul>
</div>
</template>
<script>
import { s__, sprintf } from '~/locale';
import { GlTab, GlTabs } from '@gitlab/ui';
import CodeInstruction from './code_instruction.vue';
export default {
name: 'MavenInstallation',
components: {
CodeInstruction,
GlTab,
GlTabs,
},
props: {
heading: {
type: String,
default: s__('PackageRegistry|Package installation'),
required: false,
},
mavenMetadata: {
type: Object,
required: true,
},
registryUrl: {
type: String,
required: true,
},
helpUrl: {
type: String,
required: true,
},
},
computed: {
mavenData() {
const {
app_group: appGroup = '',
app_name: appName = '',
app_version: appVersion = '',
} = this.mavenMetadata;
return {
appGroup,
appName,
appVersion,
};
},
mavenXml() {
return `<dependency>
<groupId>${this.mavenData.appGroup}</groupId>
<artifactId>${this.mavenData.appName}</artifactId>
<version>${this.mavenData.appVersion}</version>
</dependency>`;
},
mavenCommand() {
const { appGroup: group, appName: name, appVersion: version } = this.mavenData;
return `mvn dependency:get -Dartifact=${group}:${name}:${version}`;
},
mavenSetupXml() {
return `<repositories>
<repository>
<id>gitlab-maven</id>
<url>${this.registryUrl}</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>gitlab-maven</id>
<url>${this.registryUrl}</url>
</repository>
<snapshotRepository>
<id>gitlab-maven</id>
<url>${this.registryUrl}</url>
</snapshotRepository>
</distributionManagement>`;
},
helpText() {
return sprintf(
s__(
`PackageRegistry|For more information on the Maven registry, %{linkStart}see the documentation%{linkEnd}.`,
),
{
linkStart: `<a href="${this.helpUrl}" target="_blank" rel="noopener noreferer">`,
linkEnd: '</a>',
},
false,
);
},
},
i18n: {
xmlText: sprintf(
s__(
`PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block.`,
),
{
codeStart: `<code>`,
codeEnd: '</code>',
},
false,
),
setupText: sprintf(
s__(
`PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file.`,
),
{
codeStart: `<code>`,
codeEnd: '</code>',
},
false,
),
},
};
</script>
<template>
<div class="append-bottom-default">
<gl-tabs>
<gl-tab :title="s__('PackageRegistry|Installation')">
<div class="prepend-left-default append-right-default">
<p class="prepend-top-8 font-weight-bold">{{ s__('PackageRegistry|Maven XML') }}</p>
<p v-html="$options.i18n.xmlText"></p>
<code-instruction
:instruction="mavenXml"
:copy-text="s__('PackageRegistry|Copy Maven XML')"
class="js-maven-xml"
multiline
/>
<p class="prepend-top-default font-weight-bold">
{{ s__('PackageRegistry|Maven Command') }}
</p>
<code-instruction
:instruction="mavenCommand"
:copy-text="s__('PackageRegistry|Copy Maven command')"
class="js-maven-command"
/>
</div>
</gl-tab>
<gl-tab :title="s__('PackageRegistry|Registry Setup')">
<div class="prepend-left-default append-right-default">
<p v-html="$options.i18n.setupText"></p>
<code-instruction
:instruction="mavenSetupXml"
:copy-text="s__('PackageRegistry|Copy Maven registry XML')"
class="js-maven-setup-xml"
multiline
/>
<p v-html="helpText"></p>
</div>
</gl-tab>
</gl-tabs>
</div>
</template>
......@@ -4,18 +4,13 @@ import { GlTab, GlTabs } from '@gitlab/ui';
import CodeInstruction from './code_instruction.vue';
export default {
name: 'PackageInstallation',
name: 'NpmInstallation',
components: {
CodeInstruction,
GlTab,
GlTabs,
},
props: {
heading: {
type: String,
default: s__('PackageRegistry|Package installation'),
required: false,
},
name: {
type: String,
required: true,
......@@ -72,7 +67,7 @@ export default {
</script>
<template>
<div class="col-sm-6 append-bottom-default">
<div class="append-bottom-default">
<gl-tabs>
<gl-tab :title="s__('PackageRegistry|Installation')">
<div class="prepend-left-default append-right-default">
......
......@@ -24,6 +24,8 @@ export default () =>
emptySvgPath: dataset.svgPath,
npmPath: dataset.npmPath,
npmHelpPath: dataset.npmHelpPath,
mavenPath: dataset.mavenPath,
mavenHelpPath: dataset.mavenHelpPath,
};
},
render(createElement) {
......@@ -36,6 +38,8 @@ export default () =>
emptySvgPath: this.emptySvgPath,
npmPath: this.npmPath,
npmHelpPath: this.npmHelpPath,
mavenPath: this.mavenPath,
mavenHelpPath: this.mavenHelpPath,
},
});
},
......
......@@ -13,5 +13,11 @@ module EE
def npm_package_registry_url
::Gitlab::Utils.append_path(::Gitlab.config.gitlab.url, expose_path(api_v4_packages_npm_package_name_path))
end
def package_registry_project_url(project_id, registry_type = :maven)
project_api_path = expose_path(api_v4_projects_path(id: project_id))
package_registry_project_path = "#{project_api_path}/packages/#{registry_type}"
::Gitlab::Utils.append_path(::Gitlab.config.gitlab.url, package_registry_project_path)
end
end
end
......@@ -12,4 +12,6 @@
svg_path: image_path('illustrations/no-packages.svg'),
npm_path: npm_package_registry_url,
npm_help_path: help_page_path('user/packages/npm_registry/index'),
maven_path: package_registry_project_url(@project.id, :maven),
maven_help_path: help_page_path('user/packages/maven_repository/index'),
package_file_download_path: download_project_package_file_path(@project, @package_files.first) } }
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Package code instruction to match the default snapshot 1`] = `
exports[`Package code instruction multiline to match the snapshot 1`] = `
<div>
<pre>
this is some
multiline text
</pre>
</div>
`;
exports[`Package code instruction single line to match the default snapshot 1`] = `
<div
class="input-group append-bottom-10"
>
......
......@@ -2,7 +2,8 @@ import { mount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
import PackagesApp from 'ee/packages/details/components/app.vue';
import PackageInformation from 'ee/packages/details/components/information.vue';
import PackageInstallation from 'ee/packages/details/components/installation.vue';
import NpmInstallation from 'ee/packages/details/components/npm_installation.vue';
import MavenInstallation from 'ee/packages/details/components/maven_installation.vue';
import { mavenPackage, mavenFiles, npmPackage, npmFiles } from '../../mock_data';
describe('PackagesApp', () => {
......@@ -16,6 +17,8 @@ describe('PackagesApp', () => {
emptySvgPath: 'empty-illustration',
npmPath: 'foo',
npmHelpPath: 'foo',
mavenPath: 'foo',
mavenHelpPath: 'foo',
};
function createComponent(props = {}) {
......@@ -35,7 +38,8 @@ describe('PackagesApp', () => {
const emptyState = () => wrapper.find('.js-package-empty-state');
const allPackageInformation = () => wrapper.findAll(PackageInformation);
const packageInformation = index => allPackageInformation().at(index);
const packageInstallation = () => wrapper.find(PackageInstallation);
const npmInstallation = () => wrapper.find(NpmInstallation);
const mavenInstallation = () => wrapper.find(MavenInstallation);
const allFileRows = () => wrapper.findAll('.js-file-row');
const firstFileDownloadLink = () => wrapper.find('.js-file-download');
const deleteButton = () => wrapper.find('.js-delete-button');
......@@ -67,6 +71,12 @@ describe('PackagesApp', () => {
expect(packageInformation(1)).toExist();
});
it('renders package installation instructions for maven packages', () => {
createComponent();
expect(mavenInstallation()).toExist();
});
it('does not render package metadata for npm as npm packages do not contain metadata', () => {
createComponent({
packageEntity: npmPackage,
......@@ -83,13 +93,13 @@ describe('PackagesApp', () => {
files: npmFiles,
});
expect(packageInstallation()).toExist();
expect(npmInstallation()).toExist();
});
it('does not render package installation instructions for non npm packages', () => {
createComponent();
expect(packageInstallation().exists()).toBe(false);
expect(npmInstallation().exists()).toBe(false);
});
it('renders a single file for an npm package as they only contain one file', () => {
......
......@@ -4,20 +4,43 @@ import CodeInstruction from 'ee/packages/details/components/code_instruction.vue
describe('Package code instruction', () => {
let wrapper;
beforeEach(() => {
const defaultProps = {
instruction: 'npm i @my-package',
copyText: 'Copy npm install command',
};
function createComponent(props = {}) {
wrapper = mount(CodeInstruction, {
propsData: {
instruction: 'npm i @my-package',
copyText: 'Copy npm install command',
...defaultProps,
...props,
},
});
});
}
afterEach(() => {
wrapper.destroy();
});
it('to match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
describe('single line', () => {
beforeEach(() => createComponent());
it('to match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
describe('multiline', () => {
beforeEach(() =>
createComponent({
instruction: 'this is some\nmultiline text',
copyText: 'Copy the command',
multiline: true,
}),
);
it('to match the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
});
import { mount } from '@vue/test-utils';
import MavenInstallation from 'ee/packages/details/components/maven_installation.vue';
import {
generateMavenCommand,
generateXmlCodeBlock,
generateMavenSetupXml,
mavenMetadata,
registryUrl,
} from '../mock_data';
describe('MavenInstallation', () => {
let wrapper;
const defaultProps = {
mavenMetadata,
registryUrl,
helpUrl: 'foo',
};
const mavenCommandStr = generateMavenCommand(mavenMetadata);
const xmlCodeBlock = generateXmlCodeBlock(mavenMetadata);
const mavenSetupXml = generateMavenSetupXml();
const xmlCode = () => wrapper.find('.js-maven-xml > pre');
const mavenCommand = () => wrapper.find('.js-maven-command > input');
const xmlSetup = () => wrapper.find('.js-maven-setup-xml > pre');
function createComponent(props = {}) {
const propsData = {
...defaultProps,
...props,
};
wrapper = mount(MavenInstallation, {
propsData,
});
}
beforeEach(() => {
createComponent();
});
afterEach(() => {
if (wrapper) wrapper.destroy();
});
describe('with empty maven metadata', () => {
beforeEach(() => {
createComponent({
mavenMetadata: {},
});
});
it('renders empty strings in the xml block', () => {
const emptyXmlBlock = generateXmlCodeBlock({});
expect(xmlCode().text()).toBe(emptyXmlBlock);
});
it('renders empty strings in the command block', () => {
const emptyMavenCommand = generateMavenCommand({});
expect(mavenCommand().element.value).toBe(emptyMavenCommand);
});
});
describe('installation commands', () => {
it('renders the correct xml block', () => {
expect(xmlCode().text()).toBe(xmlCodeBlock);
});
it('renders the correct maven command', () => {
expect(mavenCommand().element.value).toBe(mavenCommandStr);
});
});
describe('setup commands', () => {
it('renders the correct xml block', () => {
expect(xmlSetup().text()).toBe(mavenSetupXml);
});
});
});
import { mount } from '@vue/test-utils';
import PackageInstallation from 'ee/packages/details/components/installation.vue';
import NpmInstallation from 'ee/packages/details/components/npm_installation.vue';
describe('PackageInstallation', () => {
describe('NpmInstallation', () => {
let wrapper;
const packageScope = '@fake-scope';
......@@ -29,7 +29,7 @@ describe('PackageInstallation', () => {
...props,
};
wrapper = mount(PackageInstallation, {
wrapper = mount(NpmInstallation, {
propsData,
});
}
......
export const registryUrl = 'foo/registry';
export const mavenMetadata = {
app_group: 'com.test.package.app',
app_name: 'test-package-app',
app_version: '1.0.0',
};
export const generateMavenCommand = ({
app_group: appGroup = '',
app_name: appName = '',
app_version: appVersion = '',
}) => `mvn dependency:get -Dartifact=${appGroup}:${appName}:${appVersion}`;
export const generateXmlCodeBlock = ({
app_group: appGroup = '',
app_name: appName = '',
app_version: appVersion = '',
}) => `<dependency>
<groupId>${appGroup}</groupId>
<artifactId>${appName}</artifactId>
<version>${appVersion}</version>
</dependency>`;
export const generateMavenSetupXml = () => `<repositories>
<repository>
<id>gitlab-maven</id>
<url>${registryUrl}</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>gitlab-maven</id>
<url>${registryUrl}</url>
</repository>
<snapshotRepository>
<id>gitlab-maven</id>
<url>${registryUrl}</url>
</snapshotRepository>
</distributionManagement>`;
# frozen_string_literal: true
require 'spec_helper'
describe EE::PackagesHelper do
let(:base_url) { "#{Gitlab.config.gitlab.url}/api/v4/" }
describe 'package_registry_project_url' do
it 'returns maven registry url when registry_type is not provided' do
url = helper.package_registry_project_url(1)
expect(url).to eq("#{base_url}projects/1/packages/maven")
end
it 'returns specified registry url when registry_type is provided' do
url = helper.package_registry_project_url(1, :npm)
expect(url).to eq("#{base_url}projects/1/packages/npm")
end
end
end
......@@ -12053,6 +12053,18 @@ msgstr ""
msgid "Package was removed"
msgstr ""
msgid "PackageRegistry|Copy Maven XML"
msgstr ""
msgid "PackageRegistry|Copy Maven command"
msgstr ""
msgid "PackageRegistry|Copy Maven registry XML"
msgstr ""
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
msgid "PackageRegistry|Copy npm command"
msgstr ""
......@@ -12071,12 +12083,24 @@ msgstr ""
msgid "PackageRegistry|Delete package"
msgstr ""
msgid "PackageRegistry|For more information on the Maven registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
msgid "PackageRegistry|Installation"
msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
msgid "PackageRegistry|Maven Command"
msgstr ""
msgid "PackageRegistry|Maven XML"
msgstr ""
msgid "PackageRegistry|Package installation"
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment