Commit e44e8fb4 authored by Sean McGivern's avatar Sean McGivern

Merge branch '197960-package-detail-activity' into 'master'

Add initial package details activity panel

See merge request gitlab-org/gitlab!25534
parents 63c48c56 d6537e3b
---
title: Adds new activity panel to package details page
merge_request: 25534
author:
type: added
<script>
import { GlAvatar, GlTooltipDirective, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { __, s__ } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { formatDate } from '~/lib/utils/datetime_utility';
export default {
name: 'PackageActivity',
components: {
ClipboardButton,
GlAvatar,
GlIcon,
GlLink,
GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
data() {
return {
showDescription: false,
};
},
computed: {
...mapState(['packageEntity']),
...mapGetters(['packagePipeline']),
publishedDate() {
return formatDate(this.packageEntity.created_at, 'HH:MM yyyy-mm-dd');
},
},
methods: {
toggleShowDescription() {
this.showDescription = !this.showDescription;
},
},
i18n: {
showCommit: __('Show commit description'),
pipelineText: s__(
'PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}',
),
publishText: s__('PackageRegistry|Published to the repository at %{timestamp}'),
},
};
</script>
<template>
<div class="mb-3">
<h3 class="gl-font-size-16">{{ __('Activity') }}</h3>
<div ref="commit-info" class="info-well">
<div v-if="packagePipeline" class="well-segment">
<div class="d-flex align-items-center">
<gl-icon name="commit" class="d-none d-sm-block" />
<button
v-if="packagePipeline.git_commit_message"
ref="commit-message-toggle"
v-gl-tooltip
:title="$options.i18n.showCommit"
:aria-label="$options.i18n.showCommit"
class="text-expander mr-2 d-none d-sm-flex"
type="button"
@click="toggleShowDescription"
>
<gl-icon name="ellipsis_h" :size="12" />
</button>
<gl-link :href="`../../commit/${packagePipeline.sha}`">{{ packagePipeline.sha }}</gl-link>
<clipboard-button
:text="packagePipeline.sha"
:title="__('Copy commit SHA')"
css-class="border-0 text-secondary py-0"
/>
</div>
<div v-if="showDescription" ref="commit-message" class="mt-2 d-none d-sm-block">
<pre class="commit-row-description mb-0 pl-2">{{
packagePipeline.git_commit_message
}}</pre>
</div>
</div>
<div v-if="packagePipeline" ref="pipeline-info" class="well-segment">
<div class="d-flex align-items-center">
<gl-icon name="pipeline" class="mr-2 d-none d-sm-block" />
<gl-sprintf :message="$options.i18n.pipelineText">
<template #link>
&nbsp;
<gl-link :href="`../../pipelines/${packagePipeline.id}`"
>#{{ packagePipeline.id }}</gl-link
>
&nbsp;
</template>
<template #timestamp>
<span v-gl-tooltip :title="tooltipTitle(packagePipeline.created_at)">
&nbsp;{{ timeFormatted(packagePipeline.created_at) }}&nbsp;
</span>
</template>
<template #author
>{{ packagePipeline.user.name }}
<gl-avatar
class="ml-2 d-none d-sm-block"
:src="packagePipeline.user.avatar_url"
:size="24"
/></template>
</gl-sprintf>
</div>
</div>
<div class="well-segment d-flex align-items-center">
<gl-icon name="clock" class="mr-2 d-none d-sm-block" />
<gl-sprintf :message="$options.i18n.publishText">
<template #timestamp>
{{ publishedDate }}
</template>
</gl-sprintf>
</div>
</div>
</div>
</template>
...@@ -11,6 +11,7 @@ import { ...@@ -11,6 +11,7 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import _ from 'underscore'; import _ from 'underscore';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import PackageActivity from './activity.vue';
import PackageInformation from './information.vue'; import PackageInformation from './information.vue';
import PackageTitle from './package_title.vue'; import PackageTitle from './package_title.vue';
import ConanInstallation from './conan_installation.vue'; import ConanInstallation from './conan_installation.vue';
...@@ -34,6 +35,7 @@ export default { ...@@ -34,6 +35,7 @@ export default {
GlModal, GlModal,
GlTable, GlTable,
GlIcon, GlIcon,
PackageActivity,
PackageInformation, PackageInformation,
PackageTitle, PackageTitle,
ConanInstallation, ConanInstallation,
...@@ -214,6 +216,8 @@ export default { ...@@ -214,6 +216,8 @@ export default {
</div> </div>
</div> </div>
<package-activity />
<gl-table <gl-table
:fields="$options.filesTableHeaderFields" :fields="$options.filesTableHeaderFields"
:items="filesTableRows" :items="filesTableRows"
......
<script> <script>
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { mapGetters, mapState } from 'vuex';
import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
export default { export default {
name: 'PackageInformation', name: 'PackageInformation',
components: { components: {
ClipboardButton, ClipboardButton,
GlIcon,
GlLoadingIcon,
}, },
props: { props: {
heading: { heading: {
...@@ -28,17 +24,6 @@ export default { ...@@ -28,17 +24,6 @@ export default {
default: false, default: false,
}, },
}, },
computed: {
...mapState(['isLoading', 'pipelineError', 'pipelineInfo']),
...mapGetters(['packageHasPipeline']),
pipelineSha() {
if (this.pipelineInfo?.sha) {
return this.pipelineInfo.sha.substring(0, 7);
}
return '';
},
},
}; };
</script> </script>
...@@ -61,30 +46,6 @@ export default { ...@@ -61,30 +46,6 @@ export default {
/> />
</div> </div>
</li> </li>
<li v-if="packageHasPipeline" class="js-package-pipeline">
<span class="text-secondary">{{ __('Pipeline') }}</span>
<div class="pull-right">
<gl-loading-icon v-if="isLoading" class="vertical-align-middle" size="sm" />
<span v-else-if="pipelineError" class="js-pipeline-error">{{ pipelineError }}</span>
<span v-else class="js-pipeline-info">
<a :href="pipelineInfo.web_url" class="append-right-8">#{{ pipelineInfo.id }}</a>
<gl-icon name="branch" class="append-right-4 vertical-align-middle text-secondary" />
<a :href="`../../tree/${pipelineInfo.ref}`" class="append-right-8">{{
pipelineInfo.ref
}}</a>
<gl-icon name="commit" class="append-right-4 vertical-align-middle text-secondary" /><a
:href="`../../commit/${pipelineInfo.sha}`"
>{{ pipelineSha }}</a
>
<clipboard-button
v-if="pipelineSha"
:text="pipelineInfo.sha"
:title="__('Copy commit SHA')"
css-class="border-0 text-secondary py-0"
/>
</span>
</div>
</li>
</ul> </ul>
</div> </div>
</template> </template>
...@@ -7,18 +7,16 @@ Vue.use(Translate); ...@@ -7,18 +7,16 @@ Vue.use(Translate);
export default () => { export default () => {
const el = document.querySelector('#js-vue-packages-detail'); const el = document.querySelector('#js-vue-packages-detail');
const { const { package: packageJson, canDelete: canDeleteStr, ...rest } = el.dataset;
package: packageJson,
packageFiles: packageFilesJson,
canDelete: canDeleteStr,
...rest
} = el.dataset;
const packageEntity = JSON.parse(packageJson); const packageEntity = JSON.parse(packageJson);
const packageFiles = JSON.parse(packageFilesJson);
const canDelete = canDeleteStr === 'true'; const canDelete = canDeleteStr === 'true';
const store = createStore({ packageEntity, packageFiles, canDelete, ...rest }); const store = createStore({
store.dispatch('fetchPipelineInfo'); packageEntity,
packageFiles: packageEntity.package_files,
canDelete,
...rest,
});
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
......
import Api from '~/api';
import * as types from './mutation_types';
import createFlash from '~/flash';
import { s__ } from '~/locale';
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_LOADING);
export const fetchPipelineInfo = ({ state, commit, dispatch }) => {
const {
project_id: projectId,
build_info: { pipeline_id: pipelineId } = {},
} = state.packageEntity;
if (projectId && pipelineId) {
dispatch('toggleLoading');
Api.pipelineSingle(projectId, pipelineId)
.then(response => {
const { data } = response;
commit(types.SET_PIPELINE_ERROR, null);
commit(types.SET_PIPELINE_INFO, data);
})
.catch(() => {
createFlash(s__('PackageRegistry|There was an error fetching the pipeline information.'));
commit(
types.SET_PIPELINE_ERROR,
s__('PackageRegistry|Unable to fetch pipeline information'),
);
})
.finally(() => {
dispatch('toggleLoading');
});
}
};
...@@ -2,12 +2,8 @@ import { s__ } from '~/locale'; ...@@ -2,12 +2,8 @@ import { s__ } from '~/locale';
import { generateConanRecipe } from '../utils'; import { generateConanRecipe } from '../utils';
import { NpmManager } from '../constants'; import { NpmManager } from '../constants';
export const packageHasPipeline = ({ packageEntity }) => { export const packagePipeline = ({ packageEntity }) => {
if (packageEntity?.build_info?.pipeline_id) { return packageEntity?.pipeline || null;
return true;
}
return false;
}; };
export const packageTypeDisplay = ({ packageEntity }) => { export const packageTypeDisplay = ({ packageEntity }) => {
......
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import state from './state';
import * as actions from './actions';
import * as getters from './getters'; import * as getters from './getters';
import mutations from './mutations';
Vue.use(Vuex); Vue.use(Vuex);
export default (initialState = {}) => export default (initialState = {}) =>
new Vuex.Store({ new Vuex.Store({
actions,
getters, getters,
mutations,
state: { state: {
...state(),
...initialState, ...initialState,
}, },
}); });
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
export const SET_PIPELINE_ERROR = 'SET_PIPELINE_ERROR';
export const SET_PIPELINE_INFO = 'SET_PIPELINE_INFO';
import * as types from './mutation_types';
export default {
[types.TOGGLE_LOADING](state) {
Object.assign(state, { isLoading: !state.isLoading });
},
[types.SET_PIPELINE_ERROR](state, pipelineError) {
Object.assign(state, { pipelineError });
},
[types.SET_PIPELINE_INFO](state, pipelineInfo) {
Object.assign(state, { pipelineInfo });
},
};
export default () => ({ export default () => ({
packageEntity: null, packageEntity: null,
packageFiles: [], packageFiles: [],
pipelineInfo: {},
pipelineError: null,
isLoading: false,
}); });
.commit-row-description {
border: 0;
border-left: 3px solid $white-dark;
}
...@@ -23,5 +23,11 @@ module EE ...@@ -23,5 +23,11 @@ module EE
package_registry_project_path = "#{project_api_path}/packages/#{registry_type}" package_registry_project_path = "#{project_api_path}/packages/#{registry_type}"
expose_url(package_registry_project_path) expose_url(package_registry_project_path)
end end
def package_from_presenter(package)
presenter = ::Packages::Detail::PackagePresenter.new(package)
presenter.detail_view.to_json
end
end end
end end
# frozen_string_literal: true
module Packages
module Detail
class PackagePresenter
def initialize(package)
@package = package
end
def detail_view
package_detail = {
created_at: @package.created_at,
name: @package.name,
package_files: @package.package_files.map { |pf| build_package_file_view(pf) },
package_type: @package.package_type,
project_id: @package.project_id,
tags: @package.tags.as_json,
updated_at: @package.updated_at,
version: @package.version
}
package_detail[:maven_metadatum] = @package.maven_metadatum if @package.maven_metadatum
package_detail[:pipeline] = build_pipeline_info(@package.build_info.pipeline) if @package.build_info
package_detail
end
def build_package_file_view(package_file)
{
created_at: package_file.created_at,
download_path: package_file.download_path,
file_name: package_file.file_name,
size: package_file.size
}
end
def build_pipeline_info(pipeline_info)
{
created_at: pipeline_info.created_at,
id: pipeline_info.id,
sha: pipeline_info.sha,
git_commit_message: pipeline_info.git_commit_message,
user: build_user_info(pipeline_info.user)
}
end
def build_user_info(user)
return unless user
{
avatar_url: user.avatar_url,
name: user.name
}
end
end
end
end
...@@ -5,8 +5,7 @@ ...@@ -5,8 +5,7 @@
.row .row
.col-12 .col-12
#js-vue-packages-detail{ data: { package: @package.to_json(include: [:conan_metadatum, :maven_metadatum, :package_files, :build_info, :tags]), #js-vue-packages-detail{ data: { package: package_from_presenter(@package),
package_files: @package_files.to_json(methods: :download_path),
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'),
...@@ -17,5 +16,4 @@ ...@@ -17,5 +16,4 @@
conan_path: package_registry_instance_url(:conan), conan_path: package_registry_instance_url(:conan),
conan_help_path: help_page_path('user/packages/conan_repository/index'), conan_help_path: help_page_path('user/packages/conan_repository/index'),
nuget_path: nuget_package_registry_url(@project.id), 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') } }
package_file_download_path: download_project_package_file_path(@project, @package_files.first) } }
...@@ -31,7 +31,10 @@ FactoryBot.define do ...@@ -31,7 +31,10 @@ FactoryBot.define do
trait :with_build do trait :with_build do
after :create do |package| after :create do |package|
create :package_build_info, package: package, pipeline: create(:ci_build, user: package.project.creator).pipeline user = package.project.creator
pipeline = create(:ci_pipeline, user: user)
create(:ci_build, user: user, pipeline: pipeline)
create :package_build_info, package: package, pipeline: pipeline
end end
end end
end end
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PackageActivity render to match the default snapshot when no pipeline 1`] = `
<div
class="mb-3"
>
<h3
class="gl-font-size-16"
>
Activity
</h3>
<div
class="info-well"
>
<!---->
<!---->
<div
class="well-segment d-flex align-items-center"
>
<gl-icon-stub
class="mr-2 d-none d-sm-block"
name="clock"
size="16"
/>
<gl-sprintf-stub
message="Published to the repository at %{timestamp}"
/>
</div>
</div>
</div>
`;
exports[`PackageActivity render to match the default snapshot when there is a pipeline 1`] = `
<div
class="mb-3"
>
<h3
class="gl-font-size-16"
>
Activity
</h3>
<div
class="info-well"
>
<div
class="well-segment"
>
<div
class="d-flex align-items-center"
>
<gl-icon-stub
class="d-none d-sm-block"
name="commit"
size="16"
/>
<!---->
<gl-link-stub
href="../../commit/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
>
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
</gl-link-stub>
<clipboard-button-stub
cssclass="border-0 text-secondary py-0"
text="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
title="Copy commit SHA"
tooltipplacement="top"
/>
</div>
<!---->
</div>
<div
class="well-segment"
>
<div
class="d-flex align-items-center"
>
<gl-icon-stub
class="mr-2 d-none d-sm-block"
name="pipeline"
size="16"
/>
<gl-sprintf-stub
message="Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
/>
</div>
</div>
<div
class="well-segment d-flex align-items-center"
>
<gl-icon-stub
class="mr-2 d-none d-sm-block"
name="clock"
size="16"
/>
<gl-sprintf-stub
message="Published to the repository at %{timestamp}"
/>
</div>
</div>
</div>
`;
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import PackageActivity from 'ee/packages/details/components/activity.vue';
import {
npmPackage,
mavenPackage as packageWithoutBuildInfo,
mockPipelineInfo,
} from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('PackageActivity', () => {
let wrapper;
let store;
function createComponent(packageEntity = packageWithoutBuildInfo, pipelineInfo = null) {
store = new Vuex.Store({
state: {
packageEntity,
},
getters: {
packagePipeline: () => pipelineInfo,
},
});
wrapper = shallowMount(PackageActivity, {
localVue,
store,
});
}
const commitMessageToggle = () => wrapper.find({ ref: 'commit-message-toggle' });
const commitMessage = () => wrapper.find({ ref: 'commit-message' });
const commitInfo = () => wrapper.find({ ref: 'commit-info' });
const pipelineInfo = () => wrapper.find({ ref: 'pipeline-info' });
afterEach(() => {
if (wrapper) wrapper.destroy();
wrapper = null;
});
describe('render', () => {
it('to match the default snapshot when no pipeline', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('to match the default snapshot when there is a pipeline', () => {
createComponent(npmPackage, mockPipelineInfo);
expect(wrapper.element).toMatchSnapshot();
});
});
describe('commit message toggle', () => {
it("does not display the commit message button when there isn't one", () => {
createComponent(npmPackage, mockPipelineInfo);
expect(commitMessageToggle().exists()).toBe(false);
expect(commitMessage().exists()).toBe(false);
});
it('displays the commit message on toggle', () => {
const commitMessageStr = 'a message';
createComponent(npmPackage, {
...mockPipelineInfo,
git_commit_message: commitMessageStr,
});
commitMessageToggle().trigger('click');
return wrapper.vm.$nextTick(() => expect(commitMessage().text()).toBe(commitMessageStr));
});
});
describe('pipeline information', () => {
it('does not display pipeline information when no build info is available', () => {
createComponent();
expect(pipelineInfo().exists()).toBe(false);
});
it('displays the pipeline information if found', () => {
createComponent(npmPackage, mockPipelineInfo);
expect(commitInfo().exists()).toBe(true);
expect(pipelineInfo().exists()).toBe(true);
});
});
});
...@@ -34,8 +34,6 @@ describe('PackagesApp', () => { ...@@ -34,8 +34,6 @@ describe('PackagesApp', () => {
isLoading: false, isLoading: false,
packageEntity, packageEntity,
packageFiles, packageFiles,
pipelineInfo: {},
pipelineError: null,
canDelete: true, canDelete: true,
destroyPath: 'destroy-package-path', destroyPath: 'destroy-package-path',
emptySvgPath: 'empty-illustration', emptySvgPath: 'empty-illustration',
......
import Vuex from 'vuex'; import { shallowMount } from '@vue/test-utils';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import PackageInformation from 'ee/packages/details/components/information.vue'; import PackageInformation from 'ee/packages/details/components/information.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { npmPackage, mavenPackage as packageWithoutBuildInfo } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('PackageInformation', () => { describe('PackageInformation', () => {
let wrapper; let wrapper;
let store;
const defaultProps = { const defaultProps = {
information: [ information: [
...@@ -29,34 +22,14 @@ describe('PackageInformation', () => { ...@@ -29,34 +22,14 @@ describe('PackageInformation', () => {
], ],
}; };
function createComponent( function createComponent(props = {}) {
props = {},
packageEntity = packageWithoutBuildInfo,
hasPipeline = false,
isLoading = false,
pipelineError = null,
) {
const propsData = { const propsData = {
...defaultProps, ...defaultProps,
...props, ...props,
}; };
store = new Vuex.Store({
state: {
isLoading,
packageEntity,
pipelineInfo: {},
pipelineError,
},
getters: {
packageHasPipeline: () => hasPipeline,
},
});
wrapper = shallowMount(PackageInformation, { wrapper = shallowMount(PackageInformation, {
localVue,
propsData, propsData,
store,
}); });
} }
...@@ -67,10 +40,6 @@ describe('PackageInformation', () => { ...@@ -67,10 +40,6 @@ describe('PackageInformation', () => {
informationSelector() informationSelector()
.at(index) .at(index)
.text(); .text();
const packagePipelineInfoListItem = () => wrapper.find('.js-package-pipeline');
const pipelineLoader = () => wrapper.find(GlLoadingIcon);
const pipelineErrorMessage = () => wrapper.find('.js-pipeline-error');
const pipelineInfoContent = () => wrapper.find('.js-pipeline-info');
afterEach(() => { afterEach(() => {
if (wrapper) wrapper.destroy(); if (wrapper) wrapper.destroy();
...@@ -119,36 +88,4 @@ describe('PackageInformation', () => { ...@@ -119,36 +88,4 @@ describe('PackageInformation', () => {
expect(copyButton().at(2).vm.text).toBe(defaultProps.information[2].value); expect(copyButton().at(2).vm.text).toBe(defaultProps.information[2].value);
}); });
}); });
describe('pipeline information', () => {
it('does not display pipeline information when no build info is available', () => {
createComponent();
expect(packagePipelineInfoListItem().exists()).toBe(false);
});
it('displays the loading spinner when fetching information', () => {
createComponent({}, npmPackage, true, true);
expect(packagePipelineInfoListItem().exists()).toBe(true);
expect(pipelineLoader().exists()).toBe(true);
});
it('displays that the pipeline error information fetching fails', () => {
const pipelineError = 'an-error-message';
createComponent({}, npmPackage, true, false, pipelineError);
expect(packagePipelineInfoListItem().exists()).toBe(true);
expect(pipelineLoader().exists()).toBe(false);
expect(pipelineErrorMessage().exists()).toBe(true);
expect(pipelineErrorMessage().text()).toBe(pipelineError);
});
it('displays the pipeline information if found', () => {
createComponent({}, npmPackage, true);
expect(packagePipelineInfoListItem().exists()).toBe(true);
expect(pipelineInfoContent().exists()).toBe(true);
});
});
}); });
import Api from '~/api';
import * as actions from 'ee/packages/details/store/actions';
import * as types from 'ee/packages/details/store/mutation_types';
import testAction from 'helpers/vuex_action_helper';
import createFlash from '~/flash';
import { mockPipelineInfo, npmPackage } from '../../mock_data';
jest.mock('~/api.js');
jest.mock('~/flash.js');
describe('Actions PackageDetails Store', () => {
let state;
const defaultState = {
packageEntity: npmPackage,
};
beforeEach(() => {
state = defaultState;
});
describe('fetch pipeline info', () => {
it('sets pipelineError to null and pipelineInfo to the returned data', done => {
Api.pipelineSingle = jest.fn().mockResolvedValue({ data: mockPipelineInfo });
testAction(
actions.fetchPipelineInfo,
null,
state,
[
{ type: types.SET_PIPELINE_ERROR, payload: null },
{ type: types.SET_PIPELINE_INFO, payload: mockPipelineInfo },
],
[{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
done,
);
});
it('should create flash on API error and set pipelineError', done => {
Api.pipelineSingle = jest.fn().mockRejectedValue();
testAction(
actions.fetchPipelineInfo,
null,
state,
[
{
type: types.SET_PIPELINE_ERROR,
payload: 'Unable to fetch pipeline information',
},
],
[{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
);
});
});
describe('toggles loading', () => {
it('sets isLoading to true', done => {
testAction(actions.toggleLoading, {}, state, [{ type: types.TOGGLE_LOADING }], [], done);
});
it('toggles isLoading to false', done => {
testAction(
actions.toggleLoading,
{},
{ ...state, isLoading: true },
[{ type: types.TOGGLE_LOADING }],
[],
done,
);
});
});
});
import { import {
conanInstallationCommand, conanInstallationCommand,
conanSetupCommand, conanSetupCommand,
packageHasPipeline, packagePipeline,
packageTypeDisplay, packageTypeDisplay,
mavenInstallationXml, mavenInstallationXml,
mavenInstallationCommand, mavenInstallationCommand,
...@@ -30,12 +30,8 @@ import { NpmManager } from 'ee/packages/details/constants'; ...@@ -30,12 +30,8 @@ import { NpmManager } from 'ee/packages/details/constants';
describe('Getters PackageDetails Store', () => { describe('Getters PackageDetails Store', () => {
let state; let state;
const mockPipelineError = 'mock-pipeline-error';
const defaultState = { const defaultState = {
packageEntity: packageWithoutBuildInfo, packageEntity: packageWithoutBuildInfo,
pipelineInfo: mockPipelineInfo,
pipelineError: mockPipelineError,
conanPath: registryUrl, conanPath: registryUrl,
mavenPath: registryUrl, mavenPath: registryUrl,
npmPath: registryUrl, npmPath: registryUrl,
...@@ -65,19 +61,22 @@ describe('Getters PackageDetails Store', () => { ...@@ -65,19 +61,22 @@ describe('Getters PackageDetails Store', () => {
const nugetInstallationCommandStr = `nuget install ${nugetPackage.name} -Source "GitLab"`; const nugetInstallationCommandStr = `nuget install ${nugetPackage.name} -Source "GitLab"`;
const nugetSetupCommandStr = `nuget source Add -Name "GitLab" -Source "${registryUrl}" -UserName <your_username> -Password <your_token>`; const nugetSetupCommandStr = `nuget source Add -Name "GitLab" -Source "${registryUrl}" -UserName <your_username> -Password <your_token>`;
describe('packageHasPipeline', () => { describe('packagePipeline', () => {
it('should return true when build_info and pipeline_id exist', () => { it('should return the pipeline info when pipeline exists', () => {
setupState({ setupState({
packageEntity: npmPackage, packageEntity: {
...npmPackage,
pipeline: mockPipelineInfo,
},
}); });
expect(packageHasPipeline(state)).toEqual(true); expect(packagePipeline(state)).toEqual(mockPipelineInfo);
}); });
it('should return false when build_info does not exist', () => { it('should return null when build_info does not exist', () => {
setupState(); setupState();
expect(packageHasPipeline(state)).toEqual(false); expect(packagePipeline(state)).toBe(null);
}); });
}); });
......
import * as types from 'ee/packages/details/store/mutation_types';
import mutations from 'ee/packages/details/store/mutations';
import { mockPipelineInfo as pipelineInfo } from '../../mock_data';
describe('Mutations PackageDetails Store', () => {
let mockState;
const defaultState = {
packageEntity: null,
packageFiles: [],
pipelineInfo: {},
pipelineError: null,
isLoading: false,
};
beforeEach(() => {
mockState = defaultState;
});
describe('set package info', () => {
it('should set packageInfo', () => {
const expectedState = { ...mockState, pipelineInfo };
mutations[types.SET_PIPELINE_INFO](mockState, pipelineInfo);
expect(mockState.pipelineInfo).toEqual(expectedState.pipelineInfo);
});
});
describe('set pipeline error', () => {
it('should set pipelineError', () => {
const pipelineError = 'a-pipeline-error-message';
const expectedState = { ...mockState, pipelineError };
mutations[types.SET_PIPELINE_ERROR](mockState, pipelineError);
expect(mockState.pipelineError).toEqual(expectedState.pipelineError);
});
});
describe('toggle loading', () => {
it('should set to true', () => {
const expectedState = Object.assign({}, mockState, { isLoading: true });
mutations[types.TOGGLE_LOADING](mockState);
expect(mockState.isLoading).toEqual(expectedState.isLoading);
});
it('should toggle back to false', () => {
const expectedState = Object.assign({}, mockState, { isLoading: false });
mockState.isLoading = true;
mutations[types.TOGGLE_LOADING](mockState);
expect(mockState.isLoading).toEqual(expectedState.isLoading);
});
});
});
...@@ -3,6 +3,12 @@ const _links = { ...@@ -3,6 +3,12 @@ const _links = {
delete_api_path: 'bar', delete_api_path: 'bar',
}; };
export const mockPipelineInfo = {
id: 1,
ref: 'branch-name',
sha: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
};
export const mavenPackage = { export const mavenPackage = {
created_at: '2015-12-10', created_at: '2015-12-10',
id: 1, id: 1,
...@@ -49,6 +55,7 @@ export const npmPackage = { ...@@ -49,6 +55,7 @@ export const npmPackage = {
_links, _links,
build_info: { build_info: {
pipeline_id: 1, pipeline_id: 1,
pipeline: mockPipelineInfo,
}, },
}; };
...@@ -108,10 +115,3 @@ export const mockTags = [ ...@@ -108,10 +115,3 @@ export const mockTags = [
]; ];
export const packageList = [mavenPackage, { ...npmPackage, tags: mockTags }, conanPackage]; export const packageList = [mavenPackage, { ...npmPackage, tags: mockTags }, conanPackage];
export const mockPipelineInfo = {
id: 1,
ref: 'branch-name',
sha: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
web_url: 'foo',
};
# frozen_string_literal: true
require 'spec_helper'
describe ::Packages::Detail::PackagePresenter do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, creator: user) }
let_it_be(:package) { create(:npm_package, :with_build, project: project) }
let(:presenter) { described_class.new(package) }
let_it_be(:user_info) { { name: user.name, avatar_url: user.avatar_url } }
let!(:expected_package_files) do
npm_file = package.package_files.first
[{
created_at: npm_file.created_at,
download_path: npm_file.download_path,
file_name: npm_file.file_name,
size: npm_file.size
}]
end
let(:pipeline_info) do
pipeline = package.build_info.pipeline
{
created_at: pipeline.created_at,
id: pipeline.id,
sha: pipeline.sha,
git_commit_message: pipeline.git_commit_message,
user: user_info
}
end
let!(:expected_package_details) do
{
created_at: package.created_at,
name: package.name,
package_files: expected_package_files,
package_type: package.package_type,
project_id: package.project_id,
tags: package.tags.as_json,
updated_at: package.updated_at,
version: package.version
}
end
context 'detail_view' do
context 'with build_info' do
let!(:package) { create(:npm_package, :with_build, project: project) }
it 'returns details with pipeline' do
expected_package_details[:pipeline] = pipeline_info
expect(presenter.detail_view).to eq expected_package_details
end
end
context 'without build info' do
let!(:package) { create(:npm_package, project: project) }
it 'returns details without pipeline' do
expect(presenter.detail_view).to eq expected_package_details
end
end
end
it 'build_package_file_view returns correct file data' do
package_files_result = package.package_files.map { |pf| presenter.build_package_file_view(pf) }
expect(package_files_result).to eq expected_package_files
end
context 'build_pipeline_info' do
it 'returns correct data when there is pipeline_info' do
expect(presenter.build_pipeline_info(package.build_info.pipeline)).to eq pipeline_info
end
end
context 'build_user_info' do
it 'returns correct data when there is a user' do
expect(presenter.build_user_info(package.build_info.pipeline.user)).to eq user_info
end
it 'returns nil when there is not a user' do
expect(presenter.build_user_info(nil)).to eq nil
end
end
end
...@@ -13605,22 +13605,22 @@ msgstr "" ...@@ -13605,22 +13605,22 @@ msgstr ""
msgid "PackageRegistry|NuGet Command" msgid "PackageRegistry|NuGet Command"
msgstr "" msgstr ""
msgid "PackageRegistry|Registry Setup" msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr "" msgstr ""
msgid "PackageRegistry|Remove package" msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr "" msgstr ""
msgid "PackageRegistry|There are no packages yet" msgid "PackageRegistry|Registry Setup"
msgstr "" msgstr ""
msgid "PackageRegistry|There was a problem fetching the details for this package." msgid "PackageRegistry|Remove package"
msgstr "" msgstr ""
msgid "PackageRegistry|There was an error fetching the pipeline information." msgid "PackageRegistry|There are no packages yet"
msgstr "" msgstr ""
msgid "PackageRegistry|Unable to fetch pipeline information" msgid "PackageRegistry|There was a problem fetching the details for this package."
msgstr "" msgstr ""
msgid "PackageRegistry|Unable to load package" msgid "PackageRegistry|Unable to load package"
......
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