Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/jira_connect')
-rw-r--r--app/assets/javascripts/jira_connect/api.js24
-rw-r--r--app/assets/javascripts/jira_connect/components/app.vue64
-rw-r--r--app/assets/javascripts/jira_connect/components/group_item_name.vue34
-rw-r--r--app/assets/javascripts/jira_connect/components/groups_list.vue96
-rw-r--r--app/assets/javascripts/jira_connect/components/groups_list_item.vue41
-rw-r--r--app/assets/javascripts/jira_connect/components/subscriptions_list.vue109
-rw-r--r--app/assets/javascripts/jira_connect/index.js54
-rw-r--r--app/assets/javascripts/jira_connect/utils.js40
8 files changed, 292 insertions, 170 deletions
diff --git a/app/assets/javascripts/jira_connect/api.js b/app/assets/javascripts/jira_connect/api.js
index d78aba0a3f7..abf2c070e68 100644
--- a/app/assets/javascripts/jira_connect/api.js
+++ b/app/assets/javascripts/jira_connect/api.js
@@ -1,24 +1,5 @@
import axios from 'axios';
-
-export const getJwt = () => {
- return new Promise((resolve) => {
- AP.context.getToken((token) => {
- resolve(token);
- });
- });
-};
-
-export const getLocation = () => {
- return new Promise((resolve) => {
- if (typeof AP.getLocation !== 'function') {
- resolve();
- }
-
- AP.getLocation((location) => {
- resolve(location);
- });
- });
-};
+import { getJwt } from '~/jira_connect/utils';
export const addSubscription = async (addPath, namespace) => {
const jwt = await getJwt();
@@ -39,11 +20,12 @@ export const removeSubscription = async (removePath) => {
});
};
-export const fetchGroups = async (groupsPath, { page, perPage }) => {
+export const fetchGroups = async (groupsPath, { page, perPage, search }) => {
return axios.get(groupsPath, {
params: {
page,
per_page: perPage,
+ search,
},
});
};
diff --git a/app/assets/javascripts/jira_connect/components/app.vue b/app/assets/javascripts/jira_connect/components/app.vue
index fe5ad8b67d7..ff4dfb23687 100644
--- a/app/assets/javascripts/jira_connect/components/app.vue
+++ b/app/assets/javascripts/jira_connect/components/app.vue
@@ -1,27 +1,26 @@
<script>
-import { GlAlert, GlButton, GlModal, GlModalDirective, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlButton, GlLink, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
import { mapState, mapMutations } from 'vuex';
-import { getLocation } from '~/jira_connect/api';
+import { retrieveAlert, getLocation } from '~/jira_connect/utils';
import { __ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { SET_ALERT } from '../store/mutation_types';
-import { retrieveAlert } from '../utils';
import GroupsList from './groups_list.vue';
+import SubscriptionsList from './subscriptions_list.vue';
export default {
name: 'JiraConnectApp',
components: {
GlAlert,
GlButton,
- GlModal,
- GroupsList,
GlLink,
+ GlModal,
GlSprintf,
+ GroupsList,
+ SubscriptionsList,
},
directives: {
GlModalDirective,
},
- mixins: [glFeatureFlagsMixin()],
inject: {
usersPath: {
default: '',
@@ -91,37 +90,36 @@ export default {
<h2 class="gl-text-center">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
- <div
- class="jira-connect-app-body gl-display-flex gl-justify-content-space-between gl-my-7 gl-pb-4 gl-border-b-solid gl-border-b-1 gl-border-b-gray-200"
- >
- <h5 class="gl-align-self-center gl-mb-0" data-testid="new-jira-connect-ui-heading">
- {{ s__('Integrations|Linked namespaces') }}
- </h5>
- <gl-button
- v-if="usersPath"
- category="primary"
- variant="info"
- class="gl-align-self-center"
- :href="usersPathWithReturnTo"
- target="_blank"
- >{{ s__('Integrations|Sign in to add namespaces') }}</gl-button
- >
- <template v-else>
+ <div class="jira-connect-app-body gl-my-7 gl-px-5 gl-pb-4">
+ <div class="gl-display-flex gl-justify-content-end">
<gl-button
- v-gl-modal-directive="'add-namespace-modal'"
+ v-if="usersPath"
category="primary"
variant="info"
class="gl-align-self-center"
- >{{ s__('Integrations|Add namespace') }}</gl-button
- >
- <gl-modal
- modal-id="add-namespace-modal"
- :title="s__('Integrations|Link namespaces')"
- :action-cancel="$options.modal.cancelProps"
+ :href="usersPathWithReturnTo"
+ target="_blank"
+ >{{ s__('Integrations|Sign in to add namespaces') }}</gl-button
>
- <groups-list />
- </gl-modal>
- </template>
+ <template v-else>
+ <gl-button
+ v-gl-modal-directive="'add-namespace-modal'"
+ category="primary"
+ variant="info"
+ class="gl-align-self-center"
+ >{{ s__('Integrations|Add namespace') }}</gl-button
+ >
+ <gl-modal
+ modal-id="add-namespace-modal"
+ :title="s__('Integrations|Link namespaces')"
+ :action-cancel="$options.modal.cancelProps"
+ >
+ <groups-list />
+ </gl-modal>
+ </template>
+ </div>
+
+ <subscriptions-list />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/jira_connect/components/group_item_name.vue b/app/assets/javascripts/jira_connect/components/group_item_name.vue
new file mode 100644
index 00000000000..e6c172dae9e
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/components/group_item_name.vue
@@ -0,0 +1,34 @@
+<script>
+import { GlAvatar, GlIcon } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlAvatar,
+ GlIcon,
+ },
+ props: {
+ group: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-align-items-center">
+ <gl-icon name="folder-o" class="gl-mr-3" />
+ <div class="gl-display-none gl-flex-shrink-0 gl-sm-display-flex gl-mr-3">
+ <gl-avatar :size="32" shape="rect" :entity-name="group.name" :src="group.avatar_url" />
+ </div>
+
+ <div>
+ <span class="gl-mr-3 gl-text-gray-900! gl-font-weight-bold">
+ {{ group.full_name }}
+ </span>
+ <div v-if="group.description">
+ <p class="gl-mt-2! gl-mb-0 gl-text-gray-600" v-text="group.description"></p>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jira_connect/components/groups_list.vue b/app/assets/javascripts/jira_connect/components/groups_list.vue
index 69f2903388c..275ff820419 100644
--- a/app/assets/javascripts/jira_connect/components/groups_list.vue
+++ b/app/assets/javascripts/jira_connect/components/groups_list.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTabs, GlTab, GlLoadingIcon, GlPagination, GlAlert } from '@gitlab/ui';
+import { GlLoadingIcon, GlPagination, GlAlert, GlSearchBoxByType } from '@gitlab/ui';
import { fetchGroups } from '~/jira_connect/api';
import { defaultPerPage } from '~/jira_connect/constants';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
@@ -8,11 +8,10 @@ import GroupsListItem from './groups_list_item.vue';
export default {
components: {
- GlTabs,
- GlTab,
GlLoadingIcon,
GlPagination,
GlAlert,
+ GlSearchBoxByType,
GroupsListItem,
},
inject: {
@@ -23,7 +22,8 @@ export default {
data() {
return {
groups: [],
- isLoading: false,
+ isLoadingInitial: true,
+ isLoadingMore: false,
page: 1,
perPage: defaultPerPage,
totalItems: 0,
@@ -31,15 +31,18 @@ export default {
};
},
mounted() {
- this.loadGroups();
+ return this.loadGroups().finally(() => {
+ this.isLoadingInitial = false;
+ });
},
methods: {
- loadGroups() {
- this.isLoading = true;
+ loadGroups({ searchTerm } = {}) {
+ this.isLoadingMore = true;
- fetchGroups(this.groupsPath, {
+ return fetchGroups(this.groupsPath, {
page: this.page,
perPage: this.perPage,
+ search: searchTerm,
})
.then((response) => {
const { page, total } = parseIntPagination(normalizeHeaders(response.headers));
@@ -51,50 +54,61 @@ export default {
this.errorMessage = s__('Integrations|Failed to load namespaces. Please try again.');
})
.finally(() => {
- this.isLoading = false;
+ this.isLoadingMore = false;
});
},
+ onGroupSearch(searchTerm) {
+ return this.loadGroups({ searchTerm });
+ },
},
};
</script>
<template>
<div>
- <gl-alert v-if="errorMessage" class="gl-mb-6" variant="danger" @dismiss="errorMessage = null">
+ <gl-alert v-if="errorMessage" class="gl-mb-5" variant="danger" @dismiss="errorMessage = null">
{{ errorMessage }}
</gl-alert>
- <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>
+ <gl-search-box-by-type
+ class="gl-mb-5"
+ debounce="500"
+ :placeholder="__('Search by name')"
+ :is-loading="isLoadingMore"
+ @input="onGroupSearch"
+ />
+
+ <gl-loading-icon v-if="isLoadingInitial" 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 gl-border-t-1 gl-border-t-solid gl-border-t-gray-100"
+ :class="{ 'gl-opacity-5': isLoadingMore }"
+ data-testid="groups-list"
+ >
+ <groups-list-item
+ v-for="group in groups"
+ :key="group.id"
+ :group="group"
+ :disabled="isLoadingMore"
+ @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 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>
</div>
</template>
diff --git a/app/assets/javascripts/jira_connect/components/groups_list_item.vue b/app/assets/javascripts/jira_connect/components/groups_list_item.vue
index b8959a2a505..ad046920dd1 100644
--- a/app/assets/javascripts/jira_connect/components/groups_list_item.vue
+++ b/app/assets/javascripts/jira_connect/components/groups_list_item.vue
@@ -1,15 +1,15 @@
<script>
-import { GlAvatar, GlButton, GlIcon } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { addSubscription } from '~/jira_connect/api';
+import { persistAlert, reloadPage } from '~/jira_connect/utils';
import { s__ } from '~/locale';
-import { persistAlert } from '../utils';
+import GroupItemName from './group_item_name.vue';
export default {
components: {
- GlAvatar,
GlButton,
- GlIcon,
+ GroupItemName,
},
inject: {
subscriptionsPath: {
@@ -21,6 +21,11 @@ export default {
type: Object,
required: true,
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -42,7 +47,7 @@ export default {
variant: 'success',
});
- AP.navigator.reload();
+ reloadPage();
})
.catch((error) => {
this.$emit(
@@ -50,8 +55,6 @@ export default {
error?.response?.data?.error ||
s__('Integrations|Failed to link namespace. Please try again.'),
);
- })
- .finally(() => {
this.isLoading = false;
});
},
@@ -60,34 +63,22 @@ export default {
</script>
<template>
- <li class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-200">
+ <li class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
<div class="gl-display-flex gl-align-items-center gl-py-3">
- <gl-icon name="folder-o" class="gl-mr-3" />
- <div class="gl-display-none gl-flex-shrink-0 gl-sm-display-flex gl-mr-3">
- <gl-avatar :size="32" shape="rect" :entity-name="group.name" :src="group.avatar_url" />
- </div>
<div class="gl-min-w-0 gl-display-flex gl-flex-grow-1 gl-flex-shrink-1 gl-align-items-center">
<div class="gl-min-w-0 gl-flex-grow-1 flex-shrink-1">
- <div class="gl-display-flex gl-align-items-center gl-flex-wrap">
- <span
- class="gl-mr-3 gl-text-gray-900! gl-font-weight-bold"
- data-testid="group-list-item-name"
- >
- {{ group.full_name }}
- </span>
- </div>
- <div v-if="group.description" data-testid="group-list-item-description">
- <p class="gl-mt-2! gl-mb-0 gl-text-gray-600" v-text="group.description"></p>
- </div>
+ <group-item-name :group="group" />
</div>
<gl-button
category="secondary"
- variant="success"
+ variant="confirm"
:loading="isLoading"
+ :disabled="disabled"
@click.prevent="onClick"
- >{{ __('Link') }}</gl-button
>
+ {{ __('Link') }}
+ </gl-button>
</div>
</div>
</li>
diff --git a/app/assets/javascripts/jira_connect/components/subscriptions_list.vue b/app/assets/javascripts/jira_connect/components/subscriptions_list.vue
new file mode 100644
index 00000000000..a606e2edbbb
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/components/subscriptions_list.vue
@@ -0,0 +1,109 @@
+<script>
+import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
+import { isEmpty } from 'lodash';
+import { mapMutations } from 'vuex';
+import { removeSubscription } from '~/jira_connect/api';
+import { reloadPage } from '~/jira_connect/utils';
+import { __, s__ } from '~/locale';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { SET_ALERT } from '../store/mutation_types';
+import GroupItemName from './group_item_name.vue';
+
+export default {
+ components: {
+ GlButton,
+ GlEmptyState,
+ GlTable,
+ GroupItemName,
+ TimeagoTooltip,
+ },
+ inject: {
+ subscriptions: {
+ default: [],
+ },
+ },
+ data() {
+ return {
+ loadingItem: null,
+ };
+ },
+ fields: [
+ {
+ key: 'name',
+ label: s__('Integrations|Linked namespaces'),
+ },
+ {
+ key: 'created_at',
+ label: __('Added'),
+ tdClass: 'gl-vertical-align-middle! gl-w-20p',
+ },
+ {
+ key: 'actions',
+ label: '',
+ tdClass: 'gl-text-right gl-vertical-align-middle! gl-pl-0!',
+ },
+ ],
+ i18n: {
+ emptyTitle: s__('Integrations|No linked namespaces'),
+ emptyDescription: s__(
+ 'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.',
+ ),
+ unlinkError: s__('Integrations|Failed to unlink namespace. Please try again.'),
+ },
+ methods: {
+ ...mapMutations({
+ setAlert: SET_ALERT,
+ }),
+ isEmpty,
+ isLoadingItem(item) {
+ return this.loadingItem === item;
+ },
+ unlinkBtnClass(item) {
+ return this.isLoadingItem(item) ? '' : 'gl-ml-6';
+ },
+ onClick(item) {
+ this.loadingItem = item;
+
+ removeSubscription(item.unlink_path)
+ .then(() => {
+ reloadPage();
+ })
+ .catch((error) => {
+ this.setAlert({
+ message: error?.response?.data?.error || this.$options.i18n.unlinkError,
+ variant: 'danger',
+ });
+ this.loadingItem = null;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-empty-state
+ v-if="isEmpty(subscriptions)"
+ :title="$options.i18n.emptyTitle"
+ :description="$options.i18n.emptyDescription"
+ />
+ <gl-table v-else :items="subscriptions" :fields="$options.fields">
+ <template #cell(name)="{ item }">
+ <group-item-name :group="item.group" />
+ </template>
+ <template #cell(created_at)="{ item }">
+ <timeago-tooltip :time="item.created_at" />
+ </template>
+ <template #cell(actions)="{ item }">
+ <gl-button
+ :class="unlinkBtnClass(item)"
+ category="secondary"
+ :loading="isLoadingItem(item)"
+ :disabled="!isEmpty(loadingItem)"
+ @click.prevent="onClick(item)"
+ >{{ __('Unlink') }}</gl-button
+ >
+ </template>
+ </gl-table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jira_connect/index.js b/app/assets/javascripts/jira_connect/index.js
index ecdb41607a4..dc8bb3b0c77 100644
--- a/app/assets/javascripts/jira_connect/index.js
+++ b/app/assets/javascripts/jira_connect/index.js
@@ -1,25 +1,14 @@
import setConfigs from '@gitlab/ui/dist/config';
import Vue from 'vue';
-import { addSubscription, removeSubscription, getLocation } from '~/jira_connect/api';
+import { getLocation, sizeToParent } from '~/jira_connect/utils';
import GlFeatureFlagsPlugin from '~/vue_shared/gl_feature_flags_plugin';
import Translate from '~/vue_shared/translate';
import JiraConnectApp from './components/app.vue';
import createStore from './store';
-import { SET_ALERT } from './store/mutation_types';
const store = createStore();
-const reqComplete = () => {
- AP.navigator.reload();
-};
-
-const reqFailed = (res, fallbackErrorMessage) => {
- const { error = fallbackErrorMessage } = res || {};
-
- store.commit(SET_ALERT, { message: error, variant: 'danger' });
-};
-
const updateSignInLinks = async () => {
const location = await getLocation();
Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).forEach((el) => {
@@ -28,43 +17,7 @@ const updateSignInLinks = async () => {
});
};
-const initRemoveSubscriptionButtonHandlers = () => {
- Array.from(document.querySelectorAll('.js-jira-connect-remove-subscription')).forEach((el) => {
- el.addEventListener('click', function onRemoveSubscriptionClick(e) {
- e.preventDefault();
-
- const removePath = e.target.getAttribute('href');
- removeSubscription(removePath)
- .then(reqComplete)
- .catch((err) =>
- reqFailed(err.response.data, 'Failed to remove namespace. Please try again.'),
- );
- });
- });
-};
-
-const initAddSubscriptionFormHandler = () => {
- const formEl = document.querySelector('#add-subscription-form');
- if (!formEl) {
- return;
- }
-
- formEl.addEventListener('submit', function onAddSubscriptionForm(e) {
- e.preventDefault();
-
- const addPath = e.target.getAttribute('action');
- const namespace = (e.target.querySelector('#namespace-input') || {}).value;
-
- addSubscription(addPath, namespace)
- .then(reqComplete)
- .catch((err) => reqFailed(err.response.data, 'Failed to add namespace. Please try again.'));
- });
-};
-
export async function initJiraConnect() {
- initAddSubscriptionFormHandler();
- initRemoveSubscriptionButtonHandlers();
-
await updateSignInLinks();
const el = document.querySelector('.js-jira-connect-app');
@@ -76,14 +29,15 @@ export async function initJiraConnect() {
Vue.use(Translate);
Vue.use(GlFeatureFlagsPlugin);
- const { groupsPath, subscriptionsPath, usersPath } = el.dataset;
- AP.sizeToParent();
+ const { groupsPath, subscriptions, subscriptionsPath, usersPath } = el.dataset;
+ sizeToParent();
return new Vue({
el,
store,
provide: {
groupsPath,
+ subscriptions: JSON.parse(subscriptions),
subscriptionsPath,
usersPath,
},
diff --git a/app/assets/javascripts/jira_connect/utils.js b/app/assets/javascripts/jira_connect/utils.js
index 2a6c53ba42c..ecd1a31339a 100644
--- a/app/assets/javascripts/jira_connect/utils.js
+++ b/app/assets/javascripts/jira_connect/utils.js
@@ -1,6 +1,8 @@
import AccessorUtilities from '~/lib/utils/accessor';
import { ALERT_LOCALSTORAGE_KEY } from './constants';
+const isFunction = (fn) => typeof fn === 'function';
+
/**
* Persist alert data to localStorage.
*/
@@ -31,3 +33,41 @@ export const retrieveAlert = () => {
return JSON.parse(initialAlertJSON);
};
+
+export const getJwt = () => {
+ return new Promise((resolve) => {
+ if (isFunction(AP?.context?.getToken)) {
+ AP.context.getToken((token) => {
+ resolve(token);
+ });
+ } else {
+ resolve();
+ }
+ });
+};
+
+export const getLocation = () => {
+ return new Promise((resolve) => {
+ if (isFunction(AP?.getLocation)) {
+ AP.getLocation((location) => {
+ resolve(location);
+ });
+ } else {
+ resolve();
+ }
+ });
+};
+
+export const reloadPage = () => {
+ if (isFunction(AP?.navigator?.reload)) {
+ AP.navigator.reload();
+ } else {
+ window.location.reload();
+ }
+};
+
+export const sizeToParent = () => {
+ if (isFunction(AP?.sizeToParent)) {
+ AP.sizeToParent();
+ }
+};