Commit c7f850a8 authored by Nick Kipling's avatar Nick Kipling

Add versions tab to packags detail

Adds the new version tab and api request
Updated Vuex store to make api request
Updated with mutations and actions
Updated testing
parent 3c48eb49
......@@ -7,6 +7,8 @@ import {
GlTooltipDirective,
GlLink,
GlEmptyState,
GlTab,
GlTabs,
GlTable,
} from '@gitlab/ui';
import { escape } from 'lodash';
......@@ -19,13 +21,15 @@ 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 PackagesListLoader from '../../shared/components/packages_list_loader.vue';
import PackageListRow from '../../shared/components/package_list_row.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { generatePackageInfo } from '../utils';
import { __, s__, sprintf } from '~/locale';
import { PackageType, TrackingActions } from '../../shared/constants';
import { packageTypeToTrackCategory } from '../../shared/utils';
import { mapState } from 'vuex';
import { mapActions, mapState } from 'vuex';
export default {
name: 'PackagesApp',
......@@ -34,6 +38,8 @@ export default {
GlEmptyState,
GlLink,
GlModal,
GlTab,
GlTabs,
GlTable,
GlIcon,
PackageActivity,
......@@ -44,6 +50,8 @@ export default {
NpmInstallation,
NugetInstallation,
PypiInstallation,
PackagesListLoader,
PackageListRow,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -55,6 +63,7 @@ export default {
...mapState([
'packageEntity',
'packageFiles',
'isLoading',
'canDelete',
'destroyPath',
'svgPath',
......@@ -142,14 +151,23 @@ export default {
category: packageTypeToTrackCategory(this.packageEntity.package_type),
};
},
hasVersions() {
return Boolean(this.packageEntity.versions && this.packageEntity.versions.length);
},
},
methods: {
...mapActions(['fetchPackageVersions']),
formatSize(size) {
return numberToHumanSize(size);
},
cancelDelete() {
this.$refs.deleteModal.hide();
},
getPackageVersions() {
if (!this.packageEntity.versions) {
this.fetchPackageVersions();
}
},
},
i18n: {
deleteModalTitle: s__(`PackageRegistry|Delete Package Version`),
......@@ -197,52 +215,83 @@ export default {
</div>
</div>
<div class="row prepend-top-default" data-qa-selector="package_information_content">
<div class="col-sm-6">
<package-information :information="packageInformation" />
<package-information
v-if="packageMetadata"
:heading="packageMetadataTitle"
:information="packageMetadata"
:show-copy="true"
/>
</div>
<gl-tabs>
<gl-tab :title="__('Detail')">
<div class="row" data-qa-selector="package_information_content">
<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">
<component
:is="installationComponent"
v-if="installationComponent"
:name="packageEntity.name"
:registry-url="npmPath"
:help-url="npmHelpPath"
/>
</div>
</div>
<div class="col-sm-6">
<component
:is="installationComponent"
v-if="installationComponent"
:name="packageEntity.name"
:registry-url="npmPath"
:help-url="npmHelpPath"
/>
</div>
</div>
<package-activity />
<package-activity />
<gl-table
:fields="$options.filesTableHeaderFields"
:items="filesTableRows"
tbody-tr-class="js-file-row"
>
<template #cell(name)="items">
<gl-icon name="doc-code" class="space-right" />
<gl-link
:href="items.item.downloadPath"
class="js-file-download"
@click="track($options.trackingActions.PULL_PACKAGE)"
<gl-table
:fields="$options.filesTableHeaderFields"
:items="filesTableRows"
tbody-tr-class="js-file-row"
>
{{ items.item.name }}
</gl-link>
</template>
<template #cell(name)="items">
<gl-icon name="doc-code" class="space-right" />
<gl-link
:href="items.item.downloadPath"
class="js-file-download"
@click="track($options.trackingActions.PULL_PACKAGE)"
>
{{ items.item.name }}
</gl-link>
</template>
<template #cell(created)="items">
<span v-gl-tooltip :title="tooltipTitle(items.item.created)">{{
timeFormatted(items.item.created)
}}</span>
</template>
</gl-table>
<template #cell(created)="items">
<span v-gl-tooltip :title="tooltipTitle(items.item.created)">{{
timeFormatted(items.item.created)
}}</span>
</template>
</gl-table>
</gl-tab>
<gl-tab
:title="__('Versions')"
title-item-class="js-versions-tab"
@click="getPackageVersions"
>
<div v-if="isLoading && !hasVersions">
<packages-list-loader />
</div>
<div v-else-if="hasVersions">
<package-list-row
v-for="v in packageEntity.versions"
:key="v.id"
:package-entity="{ name: packageEntity.name, ...v }"
:package-link="v.id"
:disable-delete="true"
:show-package-type="false"
/>
</div>
<div v-else class="gl-mt-3">
<p data-testid="no-versions-message">
{{ s__('PackageRegistry|There are no other versions of this package.') }}
</p>
</div>
</gl-tab>
</gl-tabs>
<gl-modal ref="deleteModal" class="js-delete-modal" modal-id="delete-modal">
<template #modal-title>{{ $options.i18n.deleteModalTitle }}</template>
......
import { s__ } from '~/locale';
export const TrackingLabels = {
CODE_INSTRUCTION: 'code_instruction',
CONAN_INSTALLATION: 'conan_installation',
......@@ -35,3 +37,7 @@ export const NpmManager = {
NPM: 'npm',
YARN: 'yarn',
};
export const FETCH_PACKAGE_VERSIONS_ERROR = s__(
'PackageRegistry|Unable to fetch package version information.',
);
import Api from 'ee/api';
import createFlash from '~/flash';
import { FETCH_PACKAGE_VERSIONS_ERROR } from '../constants';
import * as types from './mutation_types';
export const fetchPackageVersions = ({ commit, state }) => {
commit(types.SET_LOADING, true);
const { project_id, id } = state.packageEntity;
return Api.projectPackage(project_id, id)
.then(({ data }) => {
if (data.versions) {
commit(types.SET_PACKAGE_VERSIONS, data.versions.reverse());
}
})
.catch(() => {
createFlash(FETCH_PACKAGE_VERSIONS_ERROR);
})
.finally(() => {
commit(types.SET_LOADING, false);
});
};
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
Vue.use(Vuex);
export default (initialState = {}) =>
new Vuex.Store({
actions,
getters,
mutations,
state: {
isLoading: false,
...initialState,
},
});
export const SET_LOADING = 'SET_LOADING';
export const SET_PACKAGE_VERSIONS = 'SET_PACKAGE_VERSIONS';
import * as types from './mutation_types';
export default {
[types.SET_LOADING](state, isLoading) {
state.isLoading = isLoading;
},
[types.SET_PACKAGE_VERSIONS](state, versions) {
state.packageEntity = {
...state.packageEntity,
versions,
};
},
};
export default () => ({
packageEntity: null,
packageFiles: [],
});
......@@ -9,6 +9,7 @@ module Packages
def detail_view
package_detail = {
id: @package.id,
created_at: @package.created_at,
name: @package.name,
package_files: @package.package_files.map { |pf| build_package_file_view(pf) },
......
......@@ -10,6 +10,8 @@ import NpmInstallation from 'ee/packages/details/components/npm_installation.vue
import MavenInstallation from 'ee/packages/details/components/maven_installation.vue';
import * as SharedUtils from 'ee/packages/shared/utils';
import { TrackingActions } from 'ee/packages/shared/constants';
import PackagesListLoader from 'ee/packages/shared/components/packages_list_loader.vue';
import PackageListRow from 'ee/packages/shared/components/package_list_row.vue';
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';
......@@ -31,10 +33,14 @@ describe('PackagesApp', () => {
let wrapper;
let store;
function createComponent(packageEntity = mavenPackage, packageFiles = mavenFiles) {
function createComponent({
packageEntity = mavenPackage,
packageFiles = mavenFiles,
isLoading = false,
} = {}) {
store = new Vuex.Store({
state: {
isLoading: false,
isLoading,
packageEntity,
packageFiles,
canDelete: true,
......@@ -43,6 +49,9 @@ describe('PackagesApp', () => {
npmPath: 'foo',
npmHelpPath: 'foo',
},
actions: {
fetchPackageVersions: () => [],
},
getters,
});
......@@ -54,6 +63,8 @@ describe('PackagesApp', () => {
GlDeprecatedButton: false,
GlLink: false,
GlModal: false,
GlTab: false,
GlTabs: false,
GlTable: false,
},
});
......@@ -73,6 +84,10 @@ describe('PackagesApp', () => {
const deleteButton = () => wrapper.find('.js-delete-button');
const deleteModal = () => wrapper.find(GlModal);
const modalDeleteButton = () => wrapper.find({ ref: 'modal-delete-button' });
const versionsTab = () => wrapper.find('.js-versions-tab > a');
const packagesLoader = () => wrapper.find(PackagesListLoader);
const packagesVersionRows = () => wrapper.findAll(PackageListRow);
const noVersionsMessage = () => wrapper.find('[data-testid="no-versions-message"]');
afterEach(() => {
wrapper.destroy();
......@@ -100,7 +115,7 @@ describe('PackagesApp', () => {
});
it('does not render package metadata for npm as npm packages do not contain metadata', () => {
createComponent(npmPackage, npmFiles);
createComponent({ packageEntity: npmPackage, packageFiles: npmFiles });
expect(packageInformation(0)).toExist();
expect(allPackageInformation()).toHaveLength(1);
......@@ -124,7 +139,7 @@ describe('PackagesApp', () => {
});
it('renders a single file for an npm package as they only contain one file', () => {
createComponent(npmPackage, npmFiles);
createComponent({ packageEntity: npmPackage, packageFiles: npmFiles });
expect(allFileRows()).toExist();
expect(allFileRows()).toHaveLength(1);
......@@ -155,6 +170,41 @@ describe('PackagesApp', () => {
});
});
describe('versions', () => {
describe('api call', () => {
beforeEach(() => {
createComponent();
});
it('makes api request on first click of tab', () => {
const apiMethodSpy = jest.spyOn(wrapper.vm, 'fetchPackageVersions');
versionsTab().trigger('click');
return wrapper.vm.$nextTick(() => {
expect(apiMethodSpy).toHaveBeenCalled();
});
});
});
it('displays the loader when state is loading', () => {
createComponent({ isLoading: true });
expect(packagesLoader().exists()).toBe(true);
});
it('displays the correct version count when the package has versions', () => {
createComponent({ packageEntity: npmPackage });
expect(packagesVersionRows()).toHaveLength(npmPackage.versions.length);
});
it('displays the no versions message when there are none', () => {
createComponent();
expect(noVersionsMessage().exists()).toBe(true);
});
});
describe('tracking', () => {
let eventSpy;
let utilSpy;
......@@ -166,13 +216,13 @@ describe('PackagesApp', () => {
});
it('tracking category calls packageTypeToTrackCategory', () => {
createComponent(conanPackage);
createComponent({ packageEntity: conanPackage });
expect(wrapper.vm.tracking.category).toBe(category);
expect(utilSpy).toHaveBeenCalledWith('conan');
});
it(`delete button on delete modal call event with ${TrackingActions.DELETE_PACKAGE}`, () => {
createComponent(conanPackage);
createComponent({ packageEntity: conanPackage });
deleteButton().trigger('click');
return wrapper.vm.$nextTick().then(() => {
modalDeleteButton().trigger('click');
......@@ -185,7 +235,7 @@ describe('PackagesApp', () => {
});
it(`file download link call event with ${TrackingActions.PULL_PACKAGE}`, () => {
createComponent(conanPackage);
createComponent({ packageEntity: conanPackage });
firstFileDownloadLink().trigger('click');
expect(eventSpy).toHaveBeenCalledWith(
category,
......
import Api from 'ee/api';
import createFlash from '~/flash';
import * as actions from 'ee/packages/details/store/actions';
import * as types from 'ee/packages/details/store/mutation_types';
import { FETCH_PACKAGE_VERSIONS_ERROR } from 'ee/packages/details/constants';
import testAction from 'helpers/vuex_action_helper';
import { npmPackage as packageEntity } from '../../mock_data';
jest.mock('~/flash.js');
jest.mock('ee/api.js');
describe('Actions Package details store', () => {
beforeEach(() => {
Api.projectPackage = jest.fn().mockResolvedValue({ data: packageEntity });
});
describe('fetchPackageVersions', () => {
it('should fetch the package versions', done => {
testAction(
actions.fetchPackageVersions,
undefined,
{ packageEntity },
[
{ type: types.SET_LOADING, payload: true },
{ type: types.SET_PACKAGE_VERSIONS, payload: packageEntity.versions },
{ type: types.SET_LOADING, payload: false },
],
[],
() => {
expect(Api.projectPackage).toHaveBeenCalledWith(
packageEntity.project_id,
packageEntity.id,
);
done();
},
);
});
it('should create flash on API error', done => {
Api.projectPackage = jest.fn().mockRejectedValue();
testAction(
actions.fetchPackageVersions,
undefined,
{ packageEntity },
[{ type: types.SET_LOADING, payload: true }, { type: types.SET_LOADING, payload: false }],
[],
() => {
expect(createFlash).toHaveBeenCalledWith(FETCH_PACKAGE_VERSIONS_ERROR);
done();
},
);
});
});
});
import mutations from 'ee/packages/details/store/mutations';
import * as types from 'ee/packages/details/store/mutation_types';
import { npmPackage as packageEntity } from '../../mock_data';
describe('Mutations package details Store', () => {
let mockState;
beforeEach(() => {
mockState = {
packageEntity,
};
});
describe('SET_LOADING', () => {
it('should set loading', () => {
mutations[types.SET_LOADING](mockState, true);
expect(mockState.isLoading).toEqual(true);
});
});
describe('SET_PACKAGE_VERSIONS', () => {
it('should set the package entity versions', () => {
const fakeVersions = [1, 2, 3];
mutations[types.SET_PACKAGE_VERSIONS](mockState, fakeVersions);
expect(mockState.packageEntity.versions).toEqual(fakeVersions);
});
});
});
......@@ -59,6 +59,7 @@ export const npmPackage = {
project_id: 1,
updated_at: '2015-12-10',
version: '',
versions: [],
_links,
pipeline: mockPipelineInfo,
};
......
......@@ -35,6 +35,7 @@ describe ::Packages::Detail::PackagePresenter do
end
let!(:expected_package_details) do
{
id: package.id,
created_at: package.created_at,
name: package.name,
package_files: expected_package_files,
......
......@@ -7386,6 +7386,9 @@ msgstr ""
msgid "Destroy"
msgstr ""
msgid "Detail"
msgstr ""
msgid "Details"
msgstr ""
......@@ -14896,6 +14899,9 @@ msgstr ""
msgid "PackageRegistry|There are no %{packageType} packages yet"
msgstr ""
msgid "PackageRegistry|There are no other versions of this package."
msgstr ""
msgid "PackageRegistry|There are no packages yet"
msgstr ""
......@@ -14905,6 +14911,9 @@ msgstr ""
msgid "PackageRegistry|To widen your search, change or remove the filters above."
msgstr ""
msgid "PackageRegistry|Unable to fetch package version information."
msgstr ""
msgid "PackageRegistry|Unable to load package"
msgstr ""
......@@ -23619,6 +23628,9 @@ msgstr ""
msgid "Version"
msgstr ""
msgid "Versions"
msgstr ""
msgid "Very helpful"
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