Commit a5091785 authored by Nick Kipling's avatar Nick Kipling Committed by Imre Farkas

Initial work to add Maven commands

Create new Maven installation component
Rename npm installation component
Add support to code block for multiline
Add components to top level app
Add tests for new components
Update tests for existing
Added changelog entry
Removed unused package url
Added setup instructions to Maven component
Added rails helper for package urls
Updated tests
parent c5ec3889
---
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