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 { ...@@ -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 <package-information :information="packageInformation" />
v-if="packageMetadata" <package-information
:heading="packageMetadataTitle" v-if="packageMetadata"
:information="packageMetadata" :heading="packageMetadataTitle"
:show-copy="true" :information="packageMetadata"
/> :show-copy="true"
<package-installation />
v-else </div>
:type="packageEntity.package_type"
:name="packageEntity.name" <div class="col-sm-6">
:registry-url="npmPath" <npm-installation
:help-url="npmHelpPath" 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> </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,26 +28,24 @@ export default { ...@@ -28,26 +28,24 @@ 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>
</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> </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> </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 = {
instruction: 'npm i @my-package',
copyText: 'Copy npm install command',
};
function createComponent(props = {}) {
wrapper = mount(CodeInstruction, { wrapper = mount(CodeInstruction, {
propsData: { propsData: {
instruction: 'npm i @my-package', ...defaultProps,
copyText: 'Copy npm install command', ...props,
}, },
}); });
}); }
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('to match the default snapshot', () => { describe('single line', () => {
expect(wrapper.element).toMatchSnapshot(); 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 { 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