Commit bde2ffe1 authored by Tim Zallmann's avatar Tim Zallmann

Merge branch '31843-contextual-documentation-to-help-users-download-npm-packages' into 'master'

Add npm / yarn install commands to package details

Closes #31843

See merge request gitlab-org/gitlab!18999
parents 5a48cd9b 4ab85455
---
title: Added installation commands for npm and yarn packages to package detail page
merge_request: 18999
author:
type: added
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ 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 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';
...@@ -27,6 +28,7 @@ export default { ...@@ -27,6 +28,7 @@ export default {
GlTable, GlTable,
Icon, Icon,
PackageInformation, PackageInformation,
PackageInstallation,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -57,6 +59,14 @@ export default { ...@@ -57,6 +59,14 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
npmPath: {
type: String,
required: true,
},
npmHelpPath: {
type: String,
required: true,
},
}, },
computed: { computed: {
isValidPackage() { isValidPackage() {
...@@ -97,6 +107,10 @@ export default { ...@@ -97,6 +107,10 @@ export default {
label: s__('Created on'), label: s__('Created on'),
value: formatDate(this.packageEntity.created_at), value: formatDate(this.packageEntity.created_at),
}, },
{
label: s__('Updated at'),
value: formatDate(this.packageEntity.updated_at),
},
]; ];
}, },
packageMetadataTitle() { packageMetadataTitle() {
...@@ -196,6 +210,13 @@ export default { ...@@ -196,6 +210,13 @@ export default {
:heading="packageMetadataTitle" :heading="packageMetadataTitle"
:information="packageMetadata" :information="packageMetadata"
/> />
<package-installation
v-else
:type="packageEntity.package_type"
:name="packageEntity.name"
:registry-url="npmPath"
:help-url="npmHelpPath"
/>
</div> </div>
<gl-table <gl-table
......
<script>
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
name: 'CodeInstruction',
components: {
ClipboardButton,
},
props: {
instruction: {
type: String,
required: true,
},
copyText: {
type: String,
required: true,
},
},
};
</script>
<template>
<div 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>
</template>
<script>
import { s__, sprintf } from '~/locale';
import { GlTab, GlTabs } from '@gitlab/ui';
import CodeInstruction from './code_instruction.vue';
export default {
name: 'PackageInstallation',
components: {
CodeInstruction,
GlTab,
GlTabs,
},
props: {
heading: {
type: String,
default: s__('PackageRegistry|Package installation'),
required: false,
},
name: {
type: String,
required: true,
},
registryUrl: {
type: String,
required: true,
},
helpUrl: {
type: String,
required: true,
},
},
computed: {
packageRegistryUrl() {
if (this.registryUrl.indexOf('package_name') > -1) {
return this.registryUrl.substring(0, this.registryUrl.lastIndexOf('package_name'));
}
return this.registryUrl;
},
npmScope() {
return this.name.substring(0, this.name.indexOf('/'));
},
npmCommand() {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
return `npm i ${this.name}`;
},
npmSetupCommand() {
return `echo ${this.npmScope}:registry=${this.packageRegistryUrl} >> .npmrc`;
},
yarnCommand() {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
return `yarn add ${this.name}`;
},
yarnSetupCommand() {
return `echo \\"${this.npmScope}:registry\\" \\"${this.packageRegistryUrl}\\" >> .yarnrc`;
},
helpText() {
return sprintf(
s__(
`PackageRegistry|You may also need to setup authentication using an auth token. %{linkStart}See
the documentation%{linkEnd} to find out more.`,
),
{
linkStart: `<a href="${this.helpUrl}" target="_blank">`,
linkEnd: '</a>',
},
false,
);
},
},
};
</script>
<template>
<div class="col-sm-6 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|npm') }}</p>
<code-instruction
:instruction="npmCommand"
:copy-text="s__('PackageRegistry|Copy npm command')"
class="js-npm-install"
/>
<p class="prepend-top-default font-weight-bold">{{ s__('PackageRegistry|yarn') }}</p>
<code-instruction
:instruction="yarnCommand"
:copy-text="s__('PackageRegistry|Copy yarn command')"
class="js-yarn-install"
/>
</div>
</gl-tab>
<gl-tab :title="s__('PackageRegistry|Registry Setup')">
<div class="prepend-left-default append-right-default">
<p class="prepend-top-8 font-weight-bold">{{ s__('PackageRegistry|npm') }}</p>
<code-instruction
:instruction="npmSetupCommand"
:copy-text="s__('PackageRegistry|Copy npm setup command')"
class="js-npm-setup"
/>
<p class="prepend-top-default font-weight-bold">{{ s__('PackageRegistry|yarn') }}</p>
<code-instruction
:instruction="yarnSetupCommand"
:copy-text="s__('PackageRegistry|Copy yarn setup command')"
class="js-yarn-setup"
/>
<p v-html="helpText"></p>
</div>
</gl-tab>
</gl-tabs>
</div>
</template>
...@@ -22,6 +22,8 @@ export default () => ...@@ -22,6 +22,8 @@ export default () =>
canDelete, canDelete,
destroyPath: dataset.destroyPath, destroyPath: dataset.destroyPath,
emptySvgPath: dataset.svgPath, emptySvgPath: dataset.svgPath,
npmPath: dataset.npmPath,
npmHelpPath: dataset.npmHelpPath,
}; };
}, },
render(createElement) { render(createElement) {
...@@ -32,6 +34,8 @@ export default () => ...@@ -32,6 +34,8 @@ export default () =>
canDelete: this.canDelete, canDelete: this.canDelete,
destroyPath: this.destroyPath, destroyPath: this.destroyPath,
emptySvgPath: this.emptySvgPath, emptySvgPath: this.emptySvgPath,
npmPath: this.npmPath,
npmHelpPath: this.npmHelpPath,
}, },
}); });
}, },
......
...@@ -9,5 +9,9 @@ module EE ...@@ -9,5 +9,9 @@ module EE
def vue_package_list_enabled_for?(subject) def vue_package_list_enabled_for?(subject)
::Feature.enabled?(:vue_package_list, subject) ::Feature.enabled?(:vue_package_list, subject)
end end
def npm_package_registry_url
::Gitlab::Utils.append_path(::Gitlab.config.gitlab.url, expose_path(api_v4_packages_npm_package_name_path))
end
end end
end end
...@@ -10,4 +10,6 @@ ...@@ -10,4 +10,6 @@
can_delete: can?(current_user, :destroy_package, @project).to_s, can_delete: can?(current_user, :destroy_package, @project).to_s,
destroy_path: project_package_path(@project, @package), destroy_path: project_package_path(@project, @package),
svg_path: image_path('illustrations/no-packages.svg'), 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'),
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
exports[`Package code instruction to match the default snapshot 1`] = `
<div
class="input-group append-bottom-10"
>
<input
class="form-control monospace"
readonly="readonly"
type="text"
/>
<span
class="input-group-append"
>
<button
class="btn input-group-text btn-secondary btn-default"
data-clipboard-text="npm i @my-package"
data-original-title="Copy npm install command"
title=""
type="button"
>
<svg
aria-hidden="true"
class="s16 ic-duplicate"
>
<use
xlink:href="#duplicate"
/>
</svg>
</button>
</span>
</div>
`;
...@@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'; ...@@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui'; import { GlModal } from '@gitlab/ui';
import PackagesApp from 'ee/packages/components/app.vue'; import PackagesApp from 'ee/packages/components/app.vue';
import PackageInformation from 'ee/packages/components/information.vue'; import PackageInformation from 'ee/packages/components/information.vue';
import PackageInstallation from 'ee/packages/components/installation.vue';
import { mavenPackage, mavenFiles, npmPackage, npmFiles } from '../mock_data'; import { mavenPackage, mavenFiles, npmPackage, npmFiles } from '../mock_data';
describe('PackagesApp', () => { describe('PackagesApp', () => {
...@@ -13,6 +14,8 @@ describe('PackagesApp', () => { ...@@ -13,6 +14,8 @@ describe('PackagesApp', () => {
canDelete: true, canDelete: true,
destroyPath: 'destroy-package-path', destroyPath: 'destroy-package-path',
emptySvgPath: 'empty-illustration', emptySvgPath: 'empty-illustration',
npmPath: 'foo',
npmHelpPath: 'foo',
}; };
function createComponent(props = {}) { function createComponent(props = {}) {
...@@ -30,6 +33,7 @@ describe('PackagesApp', () => { ...@@ -30,6 +33,7 @@ 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 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');
...@@ -71,6 +75,21 @@ describe('PackagesApp', () => { ...@@ -71,6 +75,21 @@ describe('PackagesApp', () => {
expect(allPackageInformation().length).toBe(1); expect(allPackageInformation().length).toBe(1);
}); });
it('renders package installation instructions for npm packages', () => {
createComponent({
packageEntity: npmPackage,
files: npmFiles,
});
expect(packageInstallation()).toExist();
});
it('does not render package installation instructions for non npm packages', () => {
createComponent();
expect(packageInstallation().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', () => {
createComponent({ createComponent({
packageEntity: npmPackage, packageEntity: npmPackage,
......
import { mount } from '@vue/test-utils';
import CodeInstruction from 'ee/packages/components/code_instruction.vue';
describe('Package code instruction', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(CodeInstruction, {
propsData: {
instruction: 'npm i @my-package',
copyText: 'Copy npm install command',
},
});
});
afterEach(() => {
wrapper.destroy();
});
it('to match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
import { mount } from '@vue/test-utils';
import PackageInstallation from 'ee/packages/components/installation.vue';
describe('PackageInstallation', () => {
let wrapper;
const packageScope = '@fake-scope';
const packageName = 'my-package';
const packageScopeName = `${packageScope}/${packageName}`;
const registryUrl = 'https://gitlab.com/api/v4/packages/npm/';
const defaultProps = {
name: packageScopeName,
registryUrl: `${registryUrl}package_name`,
helpUrl: 'foo',
};
const npmInstall = `npm i ${packageScopeName}`;
const npmSetup = `echo ${packageScope}:registry=${registryUrl} >> .npmrc`;
const yarnInstall = `yarn add ${packageScopeName}`;
const yarnSetup = `echo \\"${packageScope}:registry\\" \\"${registryUrl}\\" >> .yarnrc`;
const installCommand = type => wrapper.find(`.js-${type}-install > input`);
const setupCommand = type => wrapper.find(`.js-${type}-setup > input`);
function createComponent(props = {}) {
const propsData = {
...defaultProps,
...props,
};
wrapper = mount(PackageInstallation, {
propsData,
});
}
afterEach(() => {
if (wrapper) wrapper.destroy();
});
describe('registry url', () => {
it('creates the correct registry url', () => {
const testRegistryUrl = 'https://foo/baz/';
createComponent({
registryUrl: testRegistryUrl,
});
expect(wrapper.vm.packageRegistryUrl).toBe(testRegistryUrl);
});
it('creates the correct registry url when the url already contains package_name', () => {
createComponent({
registryUrl: 'https://package_name/package_name/',
});
expect(wrapper.vm.packageRegistryUrl).toBe('https://package_name/');
});
});
describe('installation commands', () => {
beforeEach(() => {
createComponent();
});
it('renders the correct npm commands', () => {
expect(installCommand('npm').element.value).toBe(npmInstall);
expect(setupCommand('npm').element.value).toBe(npmSetup);
});
it('renders the correct yarn commands', () => {
expect(installCommand('yarn').element.value).toBe(yarnInstall);
expect(setupCommand('yarn').element.value).toBe(yarnSetup);
});
});
});
...@@ -11878,15 +11878,36 @@ msgstr "" ...@@ -11878,15 +11878,36 @@ msgstr ""
msgid "Package was removed" msgid "Package was removed"
msgstr "" msgstr ""
msgid "PackageRegistry|Copy npm command"
msgstr ""
msgid "PackageRegistry|Copy npm setup command"
msgstr ""
msgid "PackageRegistry|Copy yarn command"
msgstr ""
msgid "PackageRegistry|Copy yarn setup command"
msgstr ""
msgid "PackageRegistry|Delete Package" msgid "PackageRegistry|Delete Package"
msgstr "" msgstr ""
msgid "PackageRegistry|Delete Package Version" msgid "PackageRegistry|Delete Package Version"
msgstr "" msgstr ""
msgid "PackageRegistry|Installation"
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|Package installation"
msgstr ""
msgid "PackageRegistry|Registry Setup"
msgstr ""
msgid "PackageRegistry|Remove package" msgid "PackageRegistry|Remove package"
msgstr "" msgstr ""
...@@ -11905,6 +11926,15 @@ msgstr "" ...@@ -11905,6 +11926,15 @@ msgstr ""
msgid "PackageRegistry|You are about to delete version %{boldStart}%{version}%{boldEnd} of %{boldStart}%{name}%{boldEnd}. Are you sure?" msgid "PackageRegistry|You are about to delete version %{boldStart}%{version}%{boldEnd} of %{boldStart}%{name}%{boldEnd}. Are you sure?"
msgstr "" msgstr ""
msgid "PackageRegistry|You may also need to setup authentication using an auth token. %{linkStart}See the documentation%{linkEnd} to find out more."
msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
msgid "PackageRegistry|yarn"
msgstr ""
msgid "Packages" msgid "Packages"
msgstr "" msgstr ""
...@@ -18443,6 +18473,9 @@ msgstr "" ...@@ -18443,6 +18473,9 @@ msgstr ""
msgid "Updated %{updated_at} by %{updated_by}" msgid "Updated %{updated_at} by %{updated_by}"
msgstr "" msgstr ""
msgid "Updated at"
msgstr ""
msgid "Updated to" msgid "Updated to"
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