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 { ...@@ -10,7 +10,8 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import _ from 'underscore'; import _ from 'underscore';
import PackageInformation from './information.vue'; 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 Icon from '~/vue_shared/components/icon.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
...@@ -28,7 +29,8 @@ export default { ...@@ -28,7 +29,8 @@ export default {
GlTable, GlTable,
Icon, Icon,
PackageInformation, PackageInformation,
PackageInstallation, NpmInstallation,
MavenInstallation,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -67,14 +69,24 @@ export default { ...@@ -67,14 +69,24 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
mavenPath: {
type: String,
required: true,
},
mavenHelpPath: {
type: String,
required: true,
},
}, },
computed: { computed: {
isNpmPackage() {
return this.packageEntity.package_type === PackageType.NPM;
},
isMavenPackage() {
return this.packageEntity.package_type === PackageType.MAVEN;
},
isValidPackage() { isValidPackage() {
if (this.packageEntity.name) { return Boolean(this.packageEntity.name);
return true;
}
return false;
}, },
canDeletePackage() { canDeletePackage() {
return this.canDelete && this.destroyPath; return this.canDelete && this.destroyPath;
...@@ -204,20 +216,31 @@ export default { ...@@ -204,20 +216,31 @@ export default {
</div> </div>
<div class="row prepend-top-default" data-qa-selector="package_information_content"> <div class="row prepend-top-default" data-qa-selector="package_information_content">
<package-information :type="packageEntity.package_type" :information="packageInformation" /> <div class="col-sm-6">
<package-information :information="packageInformation" />
<package-information <package-information
v-if="packageMetadata" v-if="packageMetadata"
:heading="packageMetadataTitle" :heading="packageMetadataTitle"
:information="packageMetadata" :information="packageMetadata"
:show-copy="true" :show-copy="true"
/> />
<package-installation </div>
v-else
:type="packageEntity.package_type" <div class="col-sm-6">
<npm-installation
v-if="isNpmPackage"
:name="packageEntity.name" :name="packageEntity.name"
:registry-url="npmPath" :registry-url="npmPath"
:help-url="npmHelpPath" :help-url="npmHelpPath"
/> />
<maven-installation
v-else-if="isMavenPackage"
:maven-metadata="packageEntity.maven_metadatum"
:registry-url="mavenPath"
:help-url="mavenHelpPath"
/>
</div>
</div> </div>
<gl-table <gl-table
......
...@@ -15,15 +15,24 @@ export default { ...@@ -15,15 +15,24 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
multiline: {
type: Boolean,
required: false,
default: false,
},
}, },
}; };
</script> </script>
<template> <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 /> <input :value="instruction" type="text" class="form-control monospace" readonly />
<span class="input-group-append"> <span class="input-group-append">
<clipboard-button :text="instruction" :title="copyText" class="input-group-text" /> <clipboard-button :text="instruction" :title="copyText" class="input-group-text" />
</span> </span>
</div> </div>
<div v-else>
<pre>{{ instruction }}</pre>
</div>
</template> </template>
...@@ -28,7 +28,6 @@ export default { ...@@ -28,7 +28,6 @@ export default {
</script> </script>
<template> <template>
<div class="col-sm-6">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<strong>{{ heading }}</strong> <strong>{{ heading }}</strong>
...@@ -49,5 +48,4 @@ export default { ...@@ -49,5 +48,4 @@ export default {
</li> </li>
</ul> </ul>
</div> </div>
</div>
</template> </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'; ...@@ -4,18 +4,13 @@ import { GlTab, GlTabs } from '@gitlab/ui';
import CodeInstruction from './code_instruction.vue'; import CodeInstruction from './code_instruction.vue';
export default { export default {
name: 'PackageInstallation', name: 'NpmInstallation',
components: { components: {
CodeInstruction, CodeInstruction,
GlTab, GlTab,
GlTabs, GlTabs,
}, },
props: { props: {
heading: {
type: String,
default: s__('PackageRegistry|Package installation'),
required: false,
},
name: { name: {
type: String, type: String,
required: true, required: true,
...@@ -72,7 +67,7 @@ export default { ...@@ -72,7 +67,7 @@ export default {
</script> </script>
<template> <template>
<div class="col-sm-6 append-bottom-default"> <div class="append-bottom-default">
<gl-tabs> <gl-tabs>
<gl-tab :title="s__('PackageRegistry|Installation')"> <gl-tab :title="s__('PackageRegistry|Installation')">
<div class="prepend-left-default append-right-default"> <div class="prepend-left-default append-right-default">
......
...@@ -24,6 +24,8 @@ export default () => ...@@ -24,6 +24,8 @@ export default () =>
emptySvgPath: dataset.svgPath, emptySvgPath: dataset.svgPath,
npmPath: dataset.npmPath, npmPath: dataset.npmPath,
npmHelpPath: dataset.npmHelpPath, npmHelpPath: dataset.npmHelpPath,
mavenPath: dataset.mavenPath,
mavenHelpPath: dataset.mavenHelpPath,
}; };
}, },
render(createElement) { render(createElement) {
...@@ -36,6 +38,8 @@ export default () => ...@@ -36,6 +38,8 @@ export default () =>
emptySvgPath: this.emptySvgPath, emptySvgPath: this.emptySvgPath,
npmPath: this.npmPath, npmPath: this.npmPath,
npmHelpPath: this.npmHelpPath, npmHelpPath: this.npmHelpPath,
mavenPath: this.mavenPath,
mavenHelpPath: this.mavenHelpPath,
}, },
}); });
}, },
......
...@@ -13,5 +13,11 @@ module EE ...@@ -13,5 +13,11 @@ module EE
def npm_package_registry_url def npm_package_registry_url
::Gitlab::Utils.append_path(::Gitlab.config.gitlab.url, expose_path(api_v4_packages_npm_package_name_path)) ::Gitlab::Utils.append_path(::Gitlab.config.gitlab.url, expose_path(api_v4_packages_npm_package_name_path))
end 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
end end
...@@ -12,4 +12,6 @@ ...@@ -12,4 +12,6 @@
svg_path: image_path('illustrations/no-packages.svg'), svg_path: image_path('illustrations/no-packages.svg'),
npm_path: npm_package_registry_url, npm_path: npm_package_registry_url,
npm_help_path: help_page_path('user/packages/npm_registry/index'), 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) } } package_file_download_path: download_project_package_file_path(@project, @package_files.first) } }
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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 <div
class="input-group append-bottom-10" class="input-group append-bottom-10"
> >
......
...@@ -2,7 +2,8 @@ import { mount } from '@vue/test-utils'; ...@@ -2,7 +2,8 @@ import { mount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui'; import { GlModal } from '@gitlab/ui';
import PackagesApp from 'ee/packages/details/components/app.vue'; import PackagesApp from 'ee/packages/details/components/app.vue';
import PackageInformation from 'ee/packages/details/components/information.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'; import { mavenPackage, mavenFiles, npmPackage, npmFiles } from '../../mock_data';
describe('PackagesApp', () => { describe('PackagesApp', () => {
...@@ -16,6 +17,8 @@ describe('PackagesApp', () => { ...@@ -16,6 +17,8 @@ describe('PackagesApp', () => {
emptySvgPath: 'empty-illustration', emptySvgPath: 'empty-illustration',
npmPath: 'foo', npmPath: 'foo',
npmHelpPath: 'foo', npmHelpPath: 'foo',
mavenPath: 'foo',
mavenHelpPath: 'foo',
}; };
function createComponent(props = {}) { function createComponent(props = {}) {
...@@ -35,7 +38,8 @@ describe('PackagesApp', () => { ...@@ -35,7 +38,8 @@ describe('PackagesApp', () => {
const emptyState = () => wrapper.find('.js-package-empty-state'); const emptyState = () => wrapper.find('.js-package-empty-state');
const allPackageInformation = () => wrapper.findAll(PackageInformation); const allPackageInformation = () => wrapper.findAll(PackageInformation);
const packageInformation = index => allPackageInformation().at(index); 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 allFileRows = () => wrapper.findAll('.js-file-row');
const firstFileDownloadLink = () => wrapper.find('.js-file-download'); const firstFileDownloadLink = () => wrapper.find('.js-file-download');
const deleteButton = () => wrapper.find('.js-delete-button'); const deleteButton = () => wrapper.find('.js-delete-button');
...@@ -67,6 +71,12 @@ describe('PackagesApp', () => { ...@@ -67,6 +71,12 @@ describe('PackagesApp', () => {
expect(packageInformation(1)).toExist(); 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', () => { it('does not render package metadata for npm as npm packages do not contain metadata', () => {
createComponent({ createComponent({
packageEntity: npmPackage, packageEntity: npmPackage,
...@@ -83,13 +93,13 @@ describe('PackagesApp', () => { ...@@ -83,13 +93,13 @@ describe('PackagesApp', () => {
files: npmFiles, files: npmFiles,
}); });
expect(packageInstallation()).toExist(); expect(npmInstallation()).toExist();
}); });
it('does not render package installation instructions for non npm packages', () => { it('does not render package installation instructions for non npm packages', () => {
createComponent(); 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', () => { 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 ...@@ -4,20 +4,43 @@ import CodeInstruction from 'ee/packages/details/components/code_instruction.vue
describe('Package code instruction', () => { describe('Package code instruction', () => {
let wrapper; let wrapper;
beforeEach(() => { const defaultProps = {
wrapper = mount(CodeInstruction, {
propsData: {
instruction: 'npm i @my-package', instruction: 'npm i @my-package',
copyText: 'Copy npm install command', copyText: 'Copy npm install command',
};
function createComponent(props = {}) {
wrapper = mount(CodeInstruction, {
propsData: {
...defaultProps,
...props,
}, },
}); });
}); }
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('single line', () => {
beforeEach(() => createComponent());
it('to match the default snapshot', () => { it('to match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot(); 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 { 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; let wrapper;
const packageScope = '@fake-scope'; const packageScope = '@fake-scope';
...@@ -29,7 +29,7 @@ describe('PackageInstallation', () => { ...@@ -29,7 +29,7 @@ describe('PackageInstallation', () => {
...props, ...props,
}; };
wrapper = mount(PackageInstallation, { wrapper = mount(NpmInstallation, {
propsData, 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 "" ...@@ -12053,6 +12053,18 @@ msgstr ""
msgid "Package was removed" msgid "Package was removed"
msgstr "" 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" msgid "PackageRegistry|Copy npm command"
msgstr "" msgstr ""
...@@ -12071,12 +12083,24 @@ msgstr "" ...@@ -12071,12 +12083,24 @@ msgstr ""
msgid "PackageRegistry|Delete package" msgid "PackageRegistry|Delete package"
msgstr "" 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" msgid "PackageRegistry|Installation"
msgstr "" msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab." msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr "" msgstr ""
msgid "PackageRegistry|Maven Command"
msgstr ""
msgid "PackageRegistry|Maven XML"
msgstr ""
msgid "PackageRegistry|Package installation" msgid "PackageRegistry|Package installation"
msgstr "" 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