Commit 118d7875 authored by Peter Leitzen's avatar Peter Leitzen

Merge branch '13020-add-link-functionality-to-jira-connect-app' into 'master'

Add "Link" functionality to Jira Connect app new UI

See merge request gitlab-org/gitlab!51866
parents 41fcdee9 cf3f8d01
import axios from 'axios';
const getJwt = async () => {
return AP.context.getToken();
export const getJwt = () => {
return new Promise((resolve) => {
AP.context.getToken((token) => {
resolve(token);
});
});
};
export const addSubscription = async (addPath, namespace) => {
......
<script>
import { GlTabs, GlTab, GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { GlTabs, GlTab, GlLoadingIcon, GlPagination, GlAlert } from '@gitlab/ui';
import { s__ } from '~/locale';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { fetchGroups } from '~/jira_connect/api';
......@@ -12,6 +12,7 @@ export default {
GlTab,
GlLoadingIcon,
GlPagination,
GlAlert,
GroupsListItem,
},
inject: {
......@@ -26,6 +27,7 @@ export default {
page: 1,
perPage: defaultPerPage,
totalItems: 0,
errorMessage: null,
};
},
mounted() {
......@@ -46,8 +48,7 @@ export default {
this.groups = response.data;
})
.catch(() => {
// eslint-disable-next-line no-alert
alert(s__('Integrations|Failed to load namespaces. Please try again.'));
this.errorMessage = s__('Integrations|Failed to load namespaces. Please try again.');
})
.finally(() => {
this.isLoading = false;
......@@ -58,31 +59,42 @@ export default {
</script>
<template>
<gl-tabs>
<gl-tab :title="__('Groups and subgroups')" class="gl-pt-3">
<gl-loading-icon v-if="isLoading" size="md" />
<div v-else-if="groups.length === 0" class="gl-text-center">
<h5>{{ s__('Integrations|No available namespaces.') }}</h5>
<p class="gl-mt-5">
{{
s__('Integrations|You must have owner or maintainer permissions to link namespaces.')
}}
</p>
</div>
<ul v-else class="gl-list-style-none gl-pl-0">
<groups-list-item v-for="group in groups" :key="group.id" :group="group" />
</ul>
<div>
<gl-alert v-if="errorMessage" class="gl-mb-6" variant="danger" @dismiss="errorMessage = null">
{{ errorMessage }}
</gl-alert>
<div class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-pagination
v-if="totalItems > perPage && groups.length > 0"
v-model="page"
class="gl-mb-0"
:per-page="perPage"
:total-items="totalItems"
@input="loadGroups"
/>
</div>
</gl-tab>
</gl-tabs>
<gl-tabs>
<gl-tab :title="__('Groups and subgroups')" class="gl-pt-3">
<gl-loading-icon v-if="isLoading" size="md" />
<div v-else-if="groups.length === 0" class="gl-text-center">
<h5>{{ s__('Integrations|No available namespaces.') }}</h5>
<p class="gl-mt-5">
{{
s__('Integrations|You must have owner or maintainer permissions to link namespaces.')
}}
</p>
</div>
<ul v-else class="gl-list-style-none gl-pl-0">
<groups-list-item
v-for="group in groups"
:key="group.id"
:group="group"
@error="errorMessage = $event"
/>
</ul>
<div class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-pagination
v-if="totalItems > perPage && groups.length > 0"
v-model="page"
class="gl-mb-0"
:per-page="perPage"
:total-items="totalItems"
@input="loadGroups"
/>
</div>
</gl-tab>
</gl-tabs>
</div>
</template>
<script>
import { GlIcon, GlAvatar } from '@gitlab/ui';
import { GlAvatar, GlButton, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import { addSubscription } from '~/jira_connect/api';
export default {
components: {
GlIcon,
GlAvatar,
GlButton,
GlIcon,
},
inject: {
subscriptionsPath: {
default: '',
},
},
props: {
group: {
......@@ -12,6 +21,31 @@ export default {
required: true,
},
},
data() {
return {
isLoading: false,
};
},
methods: {
onClick() {
this.isLoading = true;
addSubscription(this.subscriptionsPath, this.group.full_path)
.then(() => {
AP.navigator.reload();
})
.catch((error) => {
this.$emit(
'error',
error?.response?.data?.error ||
s__('Integrations|Failed to link namespace. Please try again.'),
);
})
.finally(() => {
this.isLoading = false;
});
},
},
};
</script>
......@@ -36,6 +70,14 @@ export default {
<p class="gl-mt-2! gl-mb-0 gl-text-gray-600" v-text="group.description"></p>
</div>
</div>
<gl-button
category="secondary"
variant="success"
:loading="isLoading"
@click.prevent="onClick"
>{{ __('Link') }}</gl-button
>
</div>
</div>
</li>
......
import Vue from 'vue';
import Vuex from 'vuex';
import $ from 'jquery';
import setConfigs from '@gitlab/ui/dist/config';
import Translate from '~/vue_shared/translate';
......@@ -10,8 +9,6 @@ import { addSubscription, removeSubscription } from '~/jira_connect/api';
import createStore from './store';
import { SET_ERROR_MESSAGE } from './store/mutation_types';
Vue.use(Vuex);
const store = createStore();
/**
......@@ -73,13 +70,14 @@ function initJiraConnect() {
Vue.use(Translate);
Vue.use(GlFeatureFlagsPlugin);
const { groupsPath } = el.dataset;
const { groupsPath, subscriptionsPath } = el.dataset;
return new Vue({
el,
store,
provide: {
groupsPath,
subscriptionsPath,
},
render(createElement) {
return createElement(JiraConnectApp);
......
import Vue from 'vue';
import Vuex from 'vuex';
import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
export default () =>
new Vuex.Store({
state,
mutations,
state,
});
......@@ -5,9 +5,14 @@ module JiraConnectHelper
Feature.enabled?(:new_jira_connect_ui, type: :development, default_enabled: :yaml)
end
def jira_connect_app_data
def jira_connect_app_data(subscriptions)
return {} unless new_jira_connect_ui?
skip_groups = subscriptions.map(&:namespace_id)
{
groups_path: api_v4_groups_path(params: { min_access_level: Gitlab::Access::MAINTAINER })
groups_path: api_v4_groups_path(params: { min_access_level: Gitlab::Access::MAINTAINER, skip_groups: skip_groups }),
subscriptions_path: jira_connect_subscriptions_path
}
end
end
......@@ -20,7 +20,7 @@
.gl-mt-5
%p Note: this integration only works with accounts on GitLab.com (SaaS).
- else
.js-jira-connect-app{ data: jira_connect_app_data }
.js-jira-connect-app{ data: jira_connect_app_data(@subscriptions) }
- unless new_jira_connect_ui?
%form#add-subscription-form.subscription-form{ action: jira_connect_subscriptions_path }
......@@ -34,7 +34,7 @@
Link namespace to Jira
- if @subscriptions.present?
%table.subscriptions
%table.subscriptions.gl-w-full
%thead
%tr
%th Namespace
......
......@@ -15410,6 +15410,9 @@ msgstr ""
msgid "Integrations|Enable comments"
msgstr ""
msgid "Integrations|Failed to link namespace. Please try again."
msgstr ""
msgid "Integrations|Failed to load namespaces. Please try again."
msgstr ""
......@@ -16986,6 +16989,9 @@ msgstr[1] ""
msgid "Line changes"
msgstr ""
msgid "Link"
msgstr ""
msgid "Link Prometheus monitoring to GitLab."
msgstr ""
......
......@@ -14,7 +14,7 @@ describe('JiraConnect API', () => {
const mockJwt = 'jwt';
const mockResponse = { success: true };
const tokenSpy = jest.fn().mockReturnValue(mockJwt);
const tokenSpy = jest.fn((callback) => callback(mockJwt));
window.AP = {
context: {
......
import { shallowMount } from '@vue/test-utils';
import { GlAvatar } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import { GlAvatar, GlButton } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mockGroup1 } from '../mock_data';
import GroupsListItem from '~/jira_connect/components/groups_list_item.vue';
import * as JiraConnectApi from '~/jira_connect/api';
describe('GroupsListItem', () => {
let wrapper;
const mockSubscriptionPath = 'subscriptionPath';
const reloadSpy = jest.fn();
global.AP = {
navigator: {
reload: reloadSpy,
},
};
const createComponent = () => {
const createComponent = ({ mountFn = shallowMount } = {}) => {
wrapper = extendedWrapper(
shallowMount(GroupsListItem, {
mountFn(GroupsListItem, {
propsData: {
group: mockGroup1,
},
provide: {
subscriptionsPath: mockSubscriptionPath,
},
}),
);
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
......@@ -30,17 +41,82 @@ describe('GroupsListItem', () => {
const findGlAvatar = () => wrapper.find(GlAvatar);
const findGroupName = () => wrapper.findByTestId('group-list-item-name');
const findGroupDescription = () => wrapper.findByTestId('group-list-item-description');
const findLinkButton = () => wrapper.find(GlButton);
const clickLinkButton = () => findLinkButton().trigger('click');
it('renders group avatar', () => {
expect(findGlAvatar().exists()).toBe(true);
expect(findGlAvatar().props('src')).toBe(mockGroup1.avatar_url);
});
describe('template', () => {
beforeEach(() => {
createComponent();
});
it('renders group avatar', () => {
expect(findGlAvatar().exists()).toBe(true);
expect(findGlAvatar().props('src')).toBe(mockGroup1.avatar_url);
});
it('renders group name', () => {
expect(findGroupName().text()).toBe(mockGroup1.full_name);
});
it('renders group name', () => {
expect(findGroupName().text()).toBe(mockGroup1.full_name);
it('renders group description', () => {
expect(findGroupDescription().text()).toBe(mockGroup1.description);
});
it('renders Link button', () => {
expect(findLinkButton().exists()).toBe(true);
expect(findLinkButton().text()).toBe('Link');
});
});
it('renders group description', () => {
expect(findGroupDescription().text()).toBe(mockGroup1.description);
describe('on Link button click', () => {
let addSubscriptionSpy;
beforeEach(() => {
createComponent({ mountFn: mount });
addSubscriptionSpy = jest.spyOn(JiraConnectApi, 'addSubscription').mockResolvedValue();
});
it('sets button to loading and sends request', async () => {
expect(findLinkButton().props('loading')).toBe(false);
clickLinkButton();
await wrapper.vm.$nextTick();
expect(findLinkButton().props('loading')).toBe(true);
expect(addSubscriptionSpy).toHaveBeenCalledWith(mockSubscriptionPath, mockGroup1.full_path);
});
describe('when request is successful', () => {
it('reloads the page', async () => {
clickLinkButton();
await waitForPromises();
expect(reloadSpy).toHaveBeenCalled();
});
});
describe('when request has errors', () => {
const mockErrorMessage = 'error message';
const mockError = { response: { data: { error: mockErrorMessage } } };
beforeEach(() => {
addSubscriptionSpy = jest
.spyOn(JiraConnectApi, 'addSubscription')
.mockRejectedValue(mockError);
});
it('emits `error` event', async () => {
clickLinkButton();
await waitForPromises();
expect(reloadSpy).not.toHaveBeenCalled();
expect(wrapper.emitted('error')[0][0]).toBe(mockErrorMessage);
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import { fetchGroups } from '~/jira_connect/api';
......@@ -28,6 +28,7 @@ describe('GroupsList', () => {
wrapper = null;
});
const findGlAlert = () => wrapper.find(GlAlert);
const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findAllItems = () => wrapper.findAll(GroupsListItem);
const findFirstItem = () => findAllItems().at(0);
......@@ -45,6 +46,18 @@ describe('GroupsList', () => {
});
});
describe('error fetching groups', () => {
it('renders error message', async () => {
fetchGroups.mockRejectedValue();
createComponent();
await waitForPromises();
expect(findGlAlert().exists()).toBe(true);
expect(findGlAlert().text()).toBe('Failed to load namespaces. Please try again.');
});
});
describe('no groups returned', () => {
it('renders empty state', async () => {
fetchGroups.mockResolvedValue(mockEmptyResponse);
......@@ -57,15 +70,28 @@ describe('GroupsList', () => {
});
describe('with groups returned', () => {
it('renders groups list', async () => {
beforeEach(async () => {
fetchGroups.mockResolvedValue({ data: [mockGroup1, mockGroup2] });
createComponent();
await waitForPromises();
});
it('renders groups list', () => {
expect(findAllItems().length).toBe(2);
expect(findFirstItem().props('group')).toBe(mockGroup1);
expect(findSecondItem().props('group')).toBe(mockGroup2);
});
it('shows error message on $emit from item', async () => {
const errorMessage = 'error message';
findFirstItem().vm.$emit('error', errorMessage);
await wrapper.vm.$nextTick();
expect(findGlAlert().exists()).toBe(true);
expect(findGlAlert().text()).toContain(errorMessage);
});
});
});
......@@ -3,6 +3,7 @@ export const mockGroup1 = {
avatar_url: 'avatar.png',
name: 'Gitlab Org',
full_name: 'Gitlab Org',
full_path: 'gitlab-org',
description: 'Open source software to collaborate on code',
};
......@@ -11,5 +12,6 @@ export const mockGroup2 = {
avatar_url: 'avatar.png',
name: 'Gitlab Com',
full_name: 'Gitlab Com',
full_path: 'gitlab-com',
description: 'For GitLab company related projects',
};
......@@ -4,12 +4,21 @@ require 'spec_helper'
RSpec.describe JiraConnectHelper do
describe '#jira_connect_app_data' do
subject { helper.jira_connect_app_data }
let_it_be(:subscription) { create(:jira_connect_subscription) }
subject { helper.jira_connect_app_data([subscription]) }
it 'includes Jira Connect app attributes' do
is_expected.to include(
:groups_path
:groups_path,
:subscriptions_path
)
end
it 'passes group as "skip_groups" param' do
skip_groups_param = CGI.escape('skip_groups[]')
expect(subject[:groups_path]).to include("#{skip_groups_param}=#{subscription.namespace.id}")
end
end
end
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