diff options
Diffstat (limited to 'app/assets/javascripts/jira_connect')
10 files changed, 285 insertions, 53 deletions
diff --git a/app/assets/javascripts/jira_connect/api.js b/app/assets/javascripts/jira_connect/api.js new file mode 100644 index 00000000000..d689a2d1962 --- /dev/null +++ b/app/assets/javascripts/jira_connect/api.js @@ -0,0 +1,33 @@ +import axios from 'axios'; + +const getJwt = async () => { + return AP.context.getToken(); +}; + +export const addSubscription = async (addPath, namespace) => { + const jwt = await getJwt(); + + return axios.post(addPath, { + jwt, + namespace_path: namespace, + }); +}; + +export const removeSubscription = async (removePath) => { + const jwt = await getJwt(); + + return axios.delete(removePath, { + params: { + jwt, + }, + }); +}; + +export const fetchGroups = async (groupsPath, { page, perPage }) => { + return axios.get(groupsPath, { + params: { + page, + per_page: perPage, + }, + }); +}; diff --git a/app/assets/javascripts/jira_connect/components/app.vue b/app/assets/javascripts/jira_connect/components/app.vue index 490bf2fdd66..f5bf30f4488 100644 --- a/app/assets/javascripts/jira_connect/components/app.vue +++ b/app/assets/javascripts/jira_connect/components/app.vue @@ -1,16 +1,63 @@ <script> +import { mapState } from 'vuex'; +import { GlAlert, GlButton, GlModal, GlModalDirective } from '@gitlab/ui'; +import { __ } from '~/locale'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import GroupsList from './groups_list.vue'; + export default { name: 'JiraConnectApp', + components: { + GlAlert, + GlButton, + GlModal, + GroupsList, + }, + directives: { + GlModalDirective, + }, + mixins: [glFeatureFlagsMixin()], computed: { - state() { - return this.$root.$data.state || {}; + ...mapState(['errorMessage']), + showNewUI() { + return this.glFeatures.newJiraConnectUi; }, - error() { - return this.state.error; + }, + modal: { + cancelProps: { + text: __('Cancel'), }, }, }; </script> + <template> - <div></div> + <div> + <gl-alert v-if="errorMessage" class="gl-mb-6" variant="danger" :dismissible="false"> + {{ errorMessage }} + </gl-alert> + + <h1>GitLab for Jira Configuration</h1> + + <div + v-if="showNewUI" + class="gl-display-flex gl-justify-content-space-between gl-my-5 gl-pb-4 gl-border-b-solid gl-border-b-1 gl-border-b-gray-200" + > + <h3 data-testid="new-jira-connect-ui-heading">{{ s__('Integrations|Linked namespaces') }}</h3> + <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> + </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 new file mode 100644 index 00000000000..eeddd32addc --- /dev/null +++ b/app/assets/javascripts/jira_connect/components/groups_list.vue @@ -0,0 +1,88 @@ +<script> +import { GlTabs, GlTab, GlLoadingIcon, GlPagination } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; +import { fetchGroups } from '~/jira_connect/api'; +import { defaultPerPage } from '~/jira_connect/constants'; +import GroupsListItem from './groups_list_item.vue'; + +export default { + components: { + GlTabs, + GlTab, + GlLoadingIcon, + GlPagination, + GroupsListItem, + }, + inject: { + groupsPath: { + default: '', + }, + }, + data() { + return { + groups: [], + isLoading: false, + page: 1, + perPage: defaultPerPage, + totalItems: 0, + }; + }, + mounted() { + this.loadGroups(); + }, + methods: { + loadGroups() { + this.isLoading = true; + + fetchGroups(this.groupsPath, { + page: this.page, + perPage: this.perPage, + }) + .then((response) => { + const { page, total } = parseIntPagination(normalizeHeaders(response.headers)); + this.page = page; + this.totalItems = total; + this.groups = response.data; + }) + .catch(() => { + // eslint-disable-next-line no-alert + alert(s__('Integrations|Failed to load namespaces. Please try again.')); + }) + .finally(() => { + this.isLoading = false; + }); + }, + }, +}; +</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 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> +</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 new file mode 100644 index 00000000000..15e37ab3cb0 --- /dev/null +++ b/app/assets/javascripts/jira_connect/components/groups_list_item.vue @@ -0,0 +1,42 @@ +<script> +import { GlIcon, GlAvatar } from '@gitlab/ui'; + +export default { + components: { + GlIcon, + GlAvatar, + }, + props: { + group: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <li class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-200"> + <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-display-sm-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> + </div> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/jira_connect/constants.js b/app/assets/javascripts/jira_connect/constants.js new file mode 100644 index 00000000000..2b3be5cd5cd --- /dev/null +++ b/app/assets/javascripts/jira_connect/constants.js @@ -0,0 +1 @@ +export const defaultPerPage = 10; diff --git a/app/assets/javascripts/jira_connect/index.js b/app/assets/javascripts/jira_connect/index.js index e7aa4c437bb..dc2a77f4e0c 100644 --- a/app/assets/javascripts/jira_connect/index.js +++ b/app/assets/javascripts/jira_connect/index.js @@ -1,18 +1,21 @@ import Vue from 'vue'; +import Vuex from 'vuex'; import $ from 'jquery'; -import App from './components/app.vue'; - -const store = { - state: { - error: '', - }, - setErrorMessage(errorMessage) { - this.state.error = errorMessage; - }, -}; +import setConfigs from '@gitlab/ui/dist/config'; +import Translate from '~/vue_shared/translate'; +import GlFeatureFlagsPlugin from '~/vue_shared/gl_feature_flags_plugin'; + +import JiraConnectApp from './components/app.vue'; +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(); /** - * Initialize necessary form handlers for the Jira Connect app + * Initialize form handlers for the Jira Connect app */ const initJiraFormHandlers = () => { const reqComplete = () => { @@ -20,53 +23,40 @@ const initJiraFormHandlers = () => { }; const reqFailed = (res, fallbackErrorMessage) => { - const { responseJSON: { error = fallbackErrorMessage } = {} } = res || {}; + const { error = fallbackErrorMessage } = res || {}; - store.setErrorMessage(error); - // eslint-disable-next-line no-alert - alert(error); + store.commit(SET_ERROR_MESSAGE, error); }; - AP.getLocation(location => { - $('.js-jira-connect-sign-in').each(function updateSignInLink() { - const updatedLink = `${$(this).attr('href')}?return_to=${location}`; - $(this).attr('href', updatedLink); + if (typeof AP.getLocation === 'function') { + AP.getLocation((location) => { + $('.js-jira-connect-sign-in').each(function updateSignInLink() { + const updatedLink = `${$(this).attr('href')}?return_to=${location}`; + $(this).attr('href', updatedLink); + }); }); - }); + } $('#add-subscription-form').on('submit', function onAddSubscriptionForm(e) { - const actionUrl = $(this).attr('action'); + const addPath = $(this).attr('action'); + const namespace = $('#namespace-input').val(); + e.preventDefault(); - AP.context.getToken(token => { - // eslint-disable-next-line no-jquery/no-ajax - $.post(actionUrl, { - jwt: token, - namespace_path: $('#namespace-input').val(), - format: 'json', - }) - .done(reqComplete) - .fail(err => reqFailed(err, 'Failed to add namespace. Please try again.')); - }); + addSubscription(addPath, namespace) + .then(reqComplete) + .catch((err) => reqFailed(err.response.data, 'Failed to add namespace. Please try again.')); }); $('.remove-subscription').on('click', function onRemoveSubscriptionClick(e) { - const href = $(this).attr('href'); + const removePath = $(this).attr('href'); e.preventDefault(); - AP.context.getToken(token => { - // eslint-disable-next-line no-jquery/no-ajax - $.ajax({ - url: href, - method: 'DELETE', - data: { - jwt: token, - format: 'json', - }, - }) - .done(reqComplete) - .fail(err => reqFailed(err, 'Failed to remove namespace. Please try again.')); - }); + removeSubscription(removePath) + .then(reqComplete) + .catch((err) => + reqFailed(err.response.data, 'Failed to remove namespace. Please try again.'), + ); }); }; @@ -75,13 +65,24 @@ function initJiraConnect() { initJiraFormHandlers(); + if (!el) { + return null; + } + + setConfigs(); + Vue.use(Translate); + Vue.use(GlFeatureFlagsPlugin); + + const { groupsPath } = el.dataset; + return new Vue({ el, - data: { - state: store.state, + store, + provide: { + groupsPath, }, render(createElement) { - return createElement(App, {}); + return createElement(JiraConnectApp); }, }); } diff --git a/app/assets/javascripts/jira_connect/store/index.js b/app/assets/javascripts/jira_connect/store/index.js new file mode 100644 index 00000000000..aa7e14269a4 --- /dev/null +++ b/app/assets/javascripts/jira_connect/store/index.js @@ -0,0 +1,9 @@ +import Vuex from 'vuex'; +import mutations from './mutations'; +import state from './state'; + +export default () => + new Vuex.Store({ + state, + mutations, + }); diff --git a/app/assets/javascripts/jira_connect/store/mutation_types.js b/app/assets/javascripts/jira_connect/store/mutation_types.js new file mode 100644 index 00000000000..7f6ff1256bb --- /dev/null +++ b/app/assets/javascripts/jira_connect/store/mutation_types.js @@ -0,0 +1 @@ +export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; diff --git a/app/assets/javascripts/jira_connect/store/mutations.js b/app/assets/javascripts/jira_connect/store/mutations.js new file mode 100644 index 00000000000..c3acd07f89f --- /dev/null +++ b/app/assets/javascripts/jira_connect/store/mutations.js @@ -0,0 +1,7 @@ +import { SET_ERROR_MESSAGE } from './mutation_types'; + +export default { + [SET_ERROR_MESSAGE](state, errorMessage) { + state.errorMessage = errorMessage; + }, +}; diff --git a/app/assets/javascripts/jira_connect/store/state.js b/app/assets/javascripts/jira_connect/store/state.js new file mode 100644 index 00000000000..079b8350770 --- /dev/null +++ b/app/assets/javascripts/jira_connect/store/state.js @@ -0,0 +1,3 @@ +export default () => ({ + errorMessage: undefined, +}); |