Commit 1fe1a2fa authored by Nick Kipling's avatar Nick Kipling Committed by Natalia Tepluhina

Adds PyPi support to frontend package details page

Created new pypi_installation component
Updated app component with small refactor
Updated utils and Vuex to support new package type
Updated package_helpers to provide urls
Added / updated tests
Updated gitlab.pot
parent c7140c77
......@@ -18,6 +18,7 @@ import ConanInstallation from './conan_installation.vue';
import MavenInstallation from './maven_installation.vue';
import NpmInstallation from './npm_installation.vue';
import NugetInstallation from './nuget_installation.vue';
import PypiInstallation from './pypi_installation.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { generatePackageInfo } from '../utils';
......@@ -42,6 +43,7 @@ export default {
MavenInstallation,
NpmInstallation,
NugetInstallation,
PypiInstallation,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -59,17 +61,21 @@ export default {
'npmPath',
'npmHelpPath',
]),
isNpmPackage() {
return this.packageEntity.package_type === PackageType.NPM;
},
isMavenPackage() {
return this.packageEntity.package_type === PackageType.MAVEN;
},
isConanPackage() {
return this.packageEntity.package_type === PackageType.CONAN;
},
isNugetPackage() {
return this.packageEntity.package_type === PackageType.NUGET;
installationComponent() {
switch (this.packageEntity.package_type) {
case PackageType.CONAN:
return ConanInstallation;
case PackageType.MAVEN:
return MavenInstallation;
case PackageType.NPM:
return NpmInstallation;
case PackageType.NUGET:
return NugetInstallation;
case PackageType.PYPI:
return PypiInstallation;
default:
return null;
}
},
isValidPackage() {
return Boolean(this.packageEntity.name);
......@@ -203,16 +209,13 @@ export default {
</div>
<div class="col-sm-6">
<npm-installation
v-if="isNpmPackage"
<component
:is="installationComponent"
v-if="installationComponent"
:name="packageEntity.name"
:registry-url="npmPath"
:help-url="npmHelpPath"
/>
<maven-installation v-else-if="isMavenPackage" />
<conan-installation v-else-if="isConanPackage" />
<nuget-installation v-else-if="isNugetPackage" />
</div>
</div>
......
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import CodeInstruction from './code_instruction.vue';
import { TrackingActions, TrackingLabels } from '../constants';
import { mapGetters, mapState } from 'vuex';
import InstallationTabs from './installation_tabs.vue';
export default {
name: 'PyPiInstallation',
components: {
CodeInstruction,
GlLink,
GlSprintf,
InstallationTabs,
},
computed: {
...mapState(['pypiHelpPath']),
...mapGetters(['pypiPipCommand', 'pypiSetupCommand']),
},
i18n: {
setupText: s__(
`PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file.`,
),
helpText: s__(
'PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}.',
),
},
trackingActions: { ...TrackingActions },
trackingLabel: TrackingLabels.PYPI_INSTALLATION,
};
</script>
<template>
<installation-tabs :tracking-label="$options.trackingLabel">
<template #installation>
<p class="prepend-top-default font-weight-bold">
{{ s__('PackageRegistry|Pip Command') }}
</p>
<code-instruction
:instruction="pypiPipCommand"
:copy-text="s__('PackageRegistry|Copy Pip command')"
data-testid="pip-command"
:tracking-action="$options.trackingActions.COPY_PIP_INSTALL_COMMAND"
/>
</template>
<template #setup>
<p>
<gl-sprintf :message="$options.i18n.setupText">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
</gl-sprintf>
</p>
<code-instruction
:instruction="pypiSetupCommand"
:copy-text="s__('PackageRegistry|Copy .pypirc content')"
data-testid="pypi-setup-content"
multiline
:tracking-action="$options.trackingActions.COPY_PYPI_SETUP_COMMAND"
/>
<gl-sprintf :message="$options.i18n.helpText">
<template #link="{ content }">
<gl-link :href="pypiHelpPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</template>
</installation-tabs>
</template>
......@@ -4,6 +4,7 @@ export const TrackingLabels = {
MAVEN_INSTALLATION: 'maven_installation',
NPM_INSTALLATION: 'npm_installation',
NUGET_INSTALLATION: 'nuget_installation',
PYPI_INSTALLATION: 'pypi_installation',
};
export const TrackingActions = {
......@@ -25,6 +26,9 @@ export const TrackingActions = {
COPY_NUGET_INSTALL_COMMAND: 'copy_nuget_install_command',
COPY_NUGET_SETUP_COMMAND: 'copy_nuget_setup_command',
COPY_PIP_INSTALL_COMMAND: 'copy_pip_install_command',
COPY_PYPI_SETUP_COMMAND: 'copy_pypi_setup_command',
};
export const NpmManager = {
......
import { generateConanRecipe } from '../utils';
import { getPackageType } from '../../shared/utils';
import { getPackageTypeLabel } from '../../shared/utils';
import { NpmManager } from '../constants';
export const packagePipeline = ({ packageEntity }) => {
......@@ -7,7 +7,7 @@ export const packagePipeline = ({ packageEntity }) => {
};
export const packageTypeDisplay = ({ packageEntity }) => {
return getPackageType(packageEntity.package_type);
return getPackageTypeLabel(packageEntity.package_type);
};
export const conanInstallationCommand = ({ packageEntity }) => {
......@@ -86,3 +86,12 @@ export const nugetInstallationCommand = ({ packageEntity }) =>
export const nugetSetupCommand = ({ nugetPath }) =>
`nuget source Add -Name "GitLab" -Source "${nugetPath}" -UserName <your_username> -Password <your_token>`;
export const pypiPipCommand = ({ pypiPath, packageEntity }) =>
// eslint-disable-next-line @gitlab/require-i18n-strings
`pip install ${packageEntity.name} --index-url ${pypiPath}`;
export const pypiSetupCommand = ({ pypiSetupPath }) => `[gitlab]
repository = ${pypiSetupPath}
username = __token__
password = <your personal access token>`;
......@@ -2,7 +2,7 @@
import PackageTags from '../../shared/components/package_tags.vue';
import PublishMethod from './publish_method.vue';
import { GlButton, GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { getPackageType } from '../../shared/utils';
import { getPackageTypeLabel } from '../../shared/utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { mapState } from 'vuex';
......@@ -31,7 +31,7 @@ export default {
isGroupPage: state => state.config.isGroupPage,
}),
packageType() {
return getPackageType(this.packageEntity.package_type);
return getPackageTypeLabel(this.packageEntity.package_type);
},
hasPipeline() {
return Boolean(this.packageEntity.pipeline);
......
import { s__ } from '~/locale';
import { TrackingCategories } from './constants';
import { PackageType, TrackingCategories } from './constants';
export const packageTypeToTrackCategory = type =>
// eslint-disable-next-line @gitlab/require-i18n-strings
......@@ -7,16 +7,18 @@ export const packageTypeToTrackCategory = type =>
export const beautifyPath = path => (path ? path.split('/').join(' / ') : '');
export const getPackageType = packageType => {
export const getPackageTypeLabel = packageType => {
switch (packageType) {
case 'conan':
case PackageType.CONAN:
return s__('PackageType|Conan');
case 'maven':
case PackageType.MAVEN:
return s__('PackageType|Maven');
case 'npm':
case PackageType.NPM:
return s__('PackageType|NPM');
case 'nuget':
case PackageType.NUGET:
return s__('PackageType|NuGet');
case PackageType.PYPI:
return s__('PackageType|PyPi');
default:
return null;
......
......@@ -25,5 +25,10 @@ module EE
presenter.detail_view.to_json
end
def pypi_registry_url(project_id)
full_url = expose_url(api_v4_projects_packages_pypi_simple_package_name_path({ id: project_id, package_name: '' }, true))
full_url.sub!('://', '://__token__:<your_personal_token>@')
end
end
end
......@@ -16,4 +16,7 @@
conan_path: package_registry_instance_url(:conan),
conan_help_path: help_page_path('user/packages/conan_repository/index'),
nuget_path: nuget_package_registry_url(@project.id),
nuget_help_path: help_page_path('user/packages/nuget_repository/index') } }
nuget_help_path: help_page_path('user/packages/nuget_repository/index'),
pypi_path: pypi_registry_url(@project.id),
pypi_setup_path: package_registry_project_url(@project.id, :pypi),
pypi_help_path: help_page_path('user/packages/pypi_repository/index') } }
---
title: Adds PyPi installation instructions to package details page
merge_request: 29935
author:
type: added
......@@ -2,6 +2,7 @@ import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import { GlEmptyState, GlModal } from '@gitlab/ui';
import Tracking from '~/tracking';
import * as getters from 'ee/packages/details/store/getters';
import PackagesApp from 'ee/packages/details/components/app.vue';
import PackageTitle from 'ee/packages/details/components/package_title.vue';
import PackageInformation from 'ee/packages/details/components/information.vue';
......@@ -11,6 +12,7 @@ import * as SharedUtils from 'ee/packages/shared/utils';
import { TrackingActions } from 'ee/packages/shared/constants';
import ConanInstallation from 'ee/packages/details/components/conan_installation.vue';
import NugetInstallation from 'ee/packages/details/components/nuget_installation.vue';
import PypiInstallation from 'ee/packages/details/components/pypi_installation.vue';
import {
conanPackage,
mavenPackage,
......@@ -18,6 +20,7 @@ import {
npmPackage,
npmFiles,
nugetPackage,
pypiPackage,
} from '../../mock_data';
import stubChildren from 'helpers/stub_children';
......@@ -40,6 +43,7 @@ describe('PackagesApp', () => {
npmPath: 'foo',
npmHelpPath: 'foo',
},
getters,
});
wrapper = mount(PackagesApp, {
......@@ -63,6 +67,7 @@ describe('PackagesApp', () => {
const mavenInstallation = () => wrapper.find(MavenInstallation);
const conanInstallation = () => wrapper.find(ConanInstallation);
const nugetInstallation = () => wrapper.find(NugetInstallation);
const pypiInstallation = () => wrapper.find(PypiInstallation);
const allFileRows = () => wrapper.findAll('.js-file-row');
const firstFileDownloadLink = () => wrapper.find('.js-file-download');
const deleteButton = () => wrapper.find('.js-delete-button');
......@@ -94,12 +99,6 @@ 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(npmPackage, npmFiles);
......@@ -107,16 +106,21 @@ describe('PackagesApp', () => {
expect(allPackageInformation()).toHaveLength(1);
});
it('renders package installation instructions for npm packages', () => {
createComponent(npmPackage, npmFiles);
describe('installation instructions', () => {
describe.each`
packageEntity | selector
${conanPackage} | ${conanInstallation}
${mavenPackage} | ${mavenInstallation}
${npmPackage} | ${npmInstallation}
${nugetPackage} | ${nugetInstallation}
${pypiPackage} | ${pypiInstallation}
`('renders', ({ packageEntity, selector }) => {
it(`${packageEntity.package_type} instructions`, () => {
createComponent({ packageEntity });
expect(npmInstallation()).toExist();
expect(selector()).toExist();
});
});
it('does not render package installation instructions for non npm packages', () => {
createComponent();
expect(npmInstallation().exists()).toBe(false);
});
it('renders a single file for an npm package as they only contain one file', () => {
......@@ -190,20 +194,4 @@ describe('PackagesApp', () => {
);
});
});
it('renders package installation instructions for conan packages', () => {
createComponent({
packageEntity: conanPackage,
});
expect(conanInstallation()).toExist();
});
it('renders package installation instructions for nuget packages', () => {
createComponent({
packageEntity: nugetPackage,
});
expect(nugetInstallation()).toExist();
});
});
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import PypiInstallation from 'ee/packages/details/components/pypi_installation.vue';
import InstallationTabs from 'ee/packages/details/components/installation_tabs.vue';
import { pypiPackage as packageEntity } from '../../mock_data';
import { GlTabs } from '@gitlab/ui';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('PypiInstallation', () => {
let wrapper;
const pipCommandStr = 'pip install';
const pypiSetupStr = 'python setup';
const store = new Vuex.Store({
state: {
packageEntity,
pypiHelpPath: 'foo',
},
getters: {
pypiPipCommand: () => pipCommandStr,
pypiSetupCommand: () => pypiSetupStr,
},
});
const findTabs = () => wrapper.find(GlTabs);
const pipCommand = () => wrapper.find('[data-testid="pip-command"]');
const setupInstruction = () => wrapper.find('[data-testid="pypi-setup-content"]');
function createComponent() {
wrapper = shallowMount(PypiInstallation, {
localVue,
store,
stubs: {
InstallationTabs,
},
});
}
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('it renders', () => {
it('with GlTabs', () => {
expect(findTabs().exists()).toBe(true);
});
});
describe('installation commands', () => {
it('renders the correct pip command', () => {
expect(pipCommand().props('instruction')).toBe(pipCommandStr);
});
});
describe('setup commands', () => {
it('renders the correct setup block', () => {
expect(setupInstruction().props('instruction')).toBe(pypiSetupStr);
});
});
});
......@@ -73,3 +73,8 @@ export const generateConanInformation = conanPackage => [
},
...generateCommonPackageInformation(conanPackage),
];
export const pypiSetupCommandStr = `[gitlab]
repository = foo
username = __token__
password = <your personal access token>`;
......@@ -10,6 +10,8 @@ import {
npmSetupCommand,
nugetInstallationCommand,
nugetSetupCommand,
pypiPipCommand,
pypiSetupCommand,
} from 'ee/packages/details/store/getters';
import {
conanPackage,
......@@ -17,12 +19,14 @@ import {
nugetPackage,
mockPipelineInfo,
mavenPackage as packageWithoutBuildInfo,
pypiPackage,
} from '../../mock_data';
import {
generateMavenCommand,
generateXmlCodeBlock,
generateMavenSetupXml,
registryUrl,
pypiSetupCommandStr,
} from '../mock_data';
import { generateConanRecipe } from 'ee/packages/details/utils';
import { NpmManager } from 'ee/packages/details/constants';
......@@ -36,6 +40,7 @@ describe('Getters PackageDetails Store', () => {
mavenPath: registryUrl,
npmPath: registryUrl,
nugetPath: registryUrl,
pypiPath: registryUrl,
};
const setupState = (testState = {}) => {
......@@ -61,6 +66,8 @@ describe('Getters PackageDetails Store', () => {
const nugetInstallationCommandStr = `nuget install ${nugetPackage.name} -Source "GitLab"`;
const nugetSetupCommandStr = `nuget source Add -Name "GitLab" -Source "${registryUrl}" -UserName <your_username> -Password <your_token>`;
const pypiPipCommandStr = `pip install ${pypiPackage.name} --index-url ${registryUrl}`;
describe('packagePipeline', () => {
it('should return the pipeline info when pipeline exists', () => {
setupState({
......@@ -87,6 +94,7 @@ describe('Getters PackageDetails Store', () => {
${packageWithoutBuildInfo} | ${'Maven'}
${npmPackage} | ${'NPM'}
${nugetPackage} | ${'NuGet'}
${pypiPackage} | ${'PyPi'}
`(`package type`, ({ packageEntity, expectedResult }) => {
beforeEach(() => setupState({ packageEntity }));
......@@ -100,13 +108,13 @@ describe('Getters PackageDetails Store', () => {
it('gets the correct conanInstallationCommand', () => {
setupState({ packageEntity: conanPackage });
expect(conanInstallationCommand(state)).toEqual(conanInstallationCommandStr);
expect(conanInstallationCommand(state)).toBe(conanInstallationCommandStr);
});
it('gets the correct conanSetupCommand', () => {
setupState({ packageEntity: conanPackage });
expect(conanSetupCommand(state)).toEqual(conanSetupCommandStr);
expect(conanSetupCommand(state)).toBe(conanSetupCommandStr);
});
});
......@@ -114,19 +122,19 @@ describe('Getters PackageDetails Store', () => {
it('gets the correct mavenInstallationXml', () => {
setupState();
expect(mavenInstallationXml(state)).toEqual(mavenInstallationXmlBlock);
expect(mavenInstallationXml(state)).toBe(mavenInstallationXmlBlock);
});
it('gets the correct mavenInstallationCommand', () => {
setupState();
expect(mavenInstallationCommand(state)).toEqual(mavenCommandStr);
expect(mavenInstallationCommand(state)).toBe(mavenCommandStr);
});
it('gets the correct mavenSetupXml', () => {
setupState();
expect(mavenSetupXml(state)).toEqual(mavenSetupXmlBlock);
expect(mavenSetupXml(state)).toBe(mavenSetupXmlBlock);
});
});
......@@ -134,25 +142,25 @@ describe('Getters PackageDetails Store', () => {
it('gets the correct npmInstallationCommand for NPM', () => {
setupState({ packageEntity: npmPackage });
expect(npmInstallationCommand(state)(NpmManager.NPM)).toEqual(npmInstallStr);
expect(npmInstallationCommand(state)(NpmManager.NPM)).toBe(npmInstallStr);
});
it('gets the correct npmSetupCommand for NPM', () => {
setupState({ packageEntity: npmPackage });
expect(npmSetupCommand(state)(NpmManager.NPM)).toEqual(npmSetupStr);
expect(npmSetupCommand(state)(NpmManager.NPM)).toBe(npmSetupStr);
});
it('gets the correct npmInstallationCommand for Yarn', () => {
setupState({ packageEntity: npmPackage });
expect(npmInstallationCommand(state)(NpmManager.YARN)).toEqual(yarnInstallStr);
expect(npmInstallationCommand(state)(NpmManager.YARN)).toBe(yarnInstallStr);
});
it('gets the correct npmSetupCommand for Yarn', () => {
setupState({ packageEntity: npmPackage });
expect(npmSetupCommand(state)(NpmManager.YARN)).toEqual(yarnSetupStr);
expect(npmSetupCommand(state)(NpmManager.YARN)).toBe(yarnSetupStr);
});
});
......@@ -160,13 +168,27 @@ describe('Getters PackageDetails Store', () => {
it('gets the correct nugetInstallationCommand', () => {
setupState({ packageEntity: nugetPackage });
expect(nugetInstallationCommand(state)).toEqual(nugetInstallationCommandStr);
expect(nugetInstallationCommand(state)).toBe(nugetInstallationCommandStr);
});
it('gets the correct nugetSetupCommand', () => {
setupState({ packageEntity: nugetPackage });
expect(nugetSetupCommand(state)).toEqual(nugetSetupCommandStr);
expect(nugetSetupCommand(state)).toBe(nugetSetupCommandStr);
});
});
describe('pypi string getters', () => {
it('gets the correct pypiPipCommand', () => {
setupState({ packageEntity: pypiPackage });
expect(pypiPipCommand(state)).toBe(pypiPipCommandStr);
});
it('gets the correct pypiSetupCommand', () => {
setupState({ pypiSetupPath: 'foo' });
expect(pypiSetupCommand(state)).toBe(pypiSetupCommandStr);
});
});
});
......@@ -99,6 +99,18 @@ export const nugetPackage = {
version: '1.0.0',
};
export const pypiPackage = {
created_at: '2015-12-10',
id: 5,
name: 'PyPiPackage',
package_files: [],
package_type: 'pypi',
project_id: 1,
tags: [],
updated_at: '2015-12-10',
version: '1.0.0',
};
export const mockTags = [
{
name: 'foo-1',
......
import { packageTypeToTrackCategory, beautifyPath, getPackageType } from 'ee/packages/shared/utils';
import {
packageTypeToTrackCategory,
beautifyPath,
getPackageTypeLabel,
} from 'ee/packages/shared/utils';
import { PackageType, TrackingCategories } from 'ee/packages/shared/constants';
describe('Packages shared utils', () => {
......@@ -24,17 +28,18 @@ describe('Packages shared utils', () => {
});
});
describe('getPackageType', () => {
describe('getPackageTypeLabel', () => {
describe.each`
packageType | expectedResult
${'conan'} | ${'Conan'}
${'maven'} | ${'Maven'}
${'npm'} | ${'NPM'}
${'nuget'} | ${'NuGet'}
${'pypi'} | ${'PyPi'}
${'foo'} | ${null}
`(`package type`, ({ packageType, expectedResult }) => {
it(`${packageType} should show as ${expectedResult}`, () => {
expect(getPackageType(packageType)).toBe(expectedResult);
expect(getPackageTypeLabel(packageType)).toBe(expectedResult);
});
});
});
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
describe EE::PackagesHelper do
let(:base_url) { "#{Gitlab.config.gitlab.url}/api/v4/" }
let_it_be(:base_url) { "#{Gitlab.config.gitlab.url}/api/v4/" }
describe 'package_registry_instance_url' do
it 'returns conant instance url when registry_type is conant' do
......@@ -32,4 +32,14 @@ describe EE::PackagesHelper do
expect(url).to eq("#{base_url}projects/1/packages/npm")
end
end
describe 'pypi_registry_url' do
let_it_be(:base_url_with_token) { base_url.sub('://', '://__token__:<your_personal_token>@') }
it 'returns the pypi registry url' do
url = helper.pypi_registry_url(1)
expect(url).to eq("#{base_url_with_token}projects/1/packages/pypi/simple")
end
end
end
......@@ -14470,6 +14470,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
msgid "PackageRegistry|Copy .pypirc content"
msgstr ""
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
......@@ -14491,6 +14494,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
msgid "PackageRegistry|Copy Pip command"
msgstr ""
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
......@@ -14524,6 +14530,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
msgid "PackageRegistry|For more information on the PyPi 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}.pypirc%{codeEnd} file."
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 ""
......@@ -14554,6 +14566,9 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
msgid "PackageRegistry|Pip Command"
msgstr ""
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
......@@ -14617,6 +14632,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
msgid "PackageType|PyPi"
msgstr ""
msgid "Packages"
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