diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-16 21:25:58 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-16 21:25:58 +0300 |
commit | a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4 (patch) | |
tree | fb69158581673816a8cd895f9d352dcb3c678b1e /app/assets/javascripts/user_lists | |
parent | d16b2e8639e99961de6ddc93909f3bb5c1445ba1 (diff) |
Add latest changes from gitlab-org/gitlab@14-0-stable-eev14.0.0-rc42
Diffstat (limited to 'app/assets/javascripts/user_lists')
8 files changed, 352 insertions, 1 deletions
diff --git a/app/assets/javascripts/user_lists/components/user_list_form.vue b/app/assets/javascripts/user_lists/components/user_list_form.vue index a0364089d68..b53aaf46ace 100644 --- a/app/assets/javascripts/user_lists/components/user_list_form.vue +++ b/app/assets/javascripts/user_lists/components/user_list_form.vue @@ -75,7 +75,7 @@ export default { </template> </gl-sprintf> </div> - <div class="gl-flex-fill-1 gl-ml-7"> + <div class="gl-flex-grow-1 gl-ml-7"> <gl-form-group label-for="user-list-name" :label="$options.translations.nameLabel" diff --git a/app/assets/javascripts/user_lists/components/user_lists.vue b/app/assets/javascripts/user_lists/components/user_lists.vue new file mode 100644 index 00000000000..80be894c689 --- /dev/null +++ b/app/assets/javascripts/user_lists/components/user_lists.vue @@ -0,0 +1,120 @@ +<script> +import { GlBadge, GlButton } from '@gitlab/ui'; +import { isEmpty } from 'lodash'; +import { mapState, mapActions } from 'vuex'; +import EmptyState from '~/feature_flags/components/empty_state.vue'; +import { + buildUrlWithCurrentLocation, + getParameterByName, + historyPushState, +} from '~/lib/utils/common_utils'; +import { objectToQuery } from '~/lib/utils/url_utility'; +import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; +import UserListsTable from './user_lists_table.vue'; + +export default { + components: { + EmptyState, + UserListsTable, + GlBadge, + GlButton, + TablePagination, + }, + inject: { + newUserListPath: { default: '' }, + }, + data() { + return { + page: getParameterByName('page') || '1', + }; + }, + computed: { + ...mapState(['userLists', 'alerts', 'count', 'pageInfo', 'isLoading', 'hasError', 'options']), + canUserRotateToken() { + return this.rotateInstanceIdPath !== ''; + }, + shouldRenderPagination() { + return ( + !this.isLoading && + !this.hasError && + this.userLists.length > 0 && + this.pageInfo.total > this.pageInfo.perPage + ); + }, + shouldShowEmptyState() { + return !this.isLoading && !this.hasError && this.userLists.length === 0; + }, + shouldRenderErrorState() { + return this.hasError && !this.isLoading; + }, + shouldRenderUserLists() { + return !this.isLoading && this.userLists.length > 0 && !this.hasError; + }, + hasNewPath() { + return !isEmpty(this.newUserListPath); + }, + }, + created() { + this.setUserListsOptions({ page: this.page }); + this.fetchUserLists(); + }, + methods: { + ...mapActions(['setUserListsOptions', 'fetchUserLists', 'clearAlert', 'deleteUserList']), + onChangePage(page) { + this.updateUserListsOptions({ + /* URLS parameters are strings, we need to parse to match types */ + page: Number(page).toString(), + }); + }, + updateUserListsOptions(parameters) { + const queryString = objectToQuery(parameters); + + historyPushState(buildUrlWithCurrentLocation(`?${queryString}`)); + this.setUserListsOptions(parameters); + this.fetchUserLists(); + }, + }, +}; +</script> +<template> + <div> + <div class="gl-display-flex gl-flex-direction-column"> + <div class="gl-display-flex gl-flex-direction-column gl-md-display-none!"> + <gl-button v-if="hasNewPath" :href="newUserListPath" variant="confirm"> + {{ s__('UserLists|New user list') }} + </gl-button> + </div> + <div + class="gl-display-flex gl-align-items-baseline gl-flex-direction-column gl-md-flex-direction-row gl-justify-content-space-between gl-mt-6" + > + <div class="gl-display-flex gl-align-items-center"> + <h2 class="gl-font-size-h2 gl-my-0"> + {{ s__('UserLists|User Lists') }} + </h2> + <gl-badge v-if="count" class="gl-ml-4">{{ count }}</gl-badge> + </div> + <div class="gl-display-flex gl-align-items-center gl-justify-content-end"> + <gl-button v-if="hasNewPath" :href="newUserListPath" variant="confirm"> + {{ s__('UserLists|New user list') }} + </gl-button> + </div> + </div> + <empty-state + :alerts="alerts" + :is-loading="isLoading" + :loading-label="s__('UserLists|Loading user lists')" + :error-state="shouldRenderErrorState" + :error-title="s__('UserLists|There was an error fetching the user lists.')" + :empty-state="shouldShowEmptyState" + :empty-title="s__('UserLists|Get started with user lists')" + :empty-description=" + s__('UserLists|User lists allow you to define a set of users to use with Feature Flags.') + " + @dismissAlert="clearAlert" + > + <user-lists-table :user-lists="userLists" @delete="deleteUserList" /> + </empty-state> + </div> + <table-pagination v-if="shouldRenderPagination" :change="onChangePage" :page-info="pageInfo" /> + </div> +</template> diff --git a/app/assets/javascripts/user_lists/components/user_lists_table.vue b/app/assets/javascripts/user_lists/components/user_lists_table.vue new file mode 100644 index 00000000000..765f59228a6 --- /dev/null +++ b/app/assets/javascripts/user_lists/components/user_lists_table.vue @@ -0,0 +1,125 @@ +<script> +import { + GlButton, + GlButtonGroup, + GlModal, + GlSprintf, + GlTooltipDirective, + GlModalDirective, +} from '@gitlab/ui'; +import { __, s__, sprintf } from '~/locale'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; + +export default { + components: { GlButton, GlButtonGroup, GlModal, GlSprintf }, + directives: { GlTooltip: GlTooltipDirective, GlModal: GlModalDirective }, + mixins: [timeagoMixin], + props: { + userLists: { + type: Array, + required: true, + }, + }, + translations: { + createdTimeagoLabel: s__('UserList|created %{timeago}'), + deleteListTitle: s__('UserList|Delete %{name}?'), + deleteListMessage: s__('User list %{name} will be removed. Are you sure?'), + editUserListLabel: s__('FeatureFlags|Edit User List'), + }, + modal: { + id: 'deleteListModal', + actionPrimary: { + text: __('Delete user list'), + attributes: { variant: 'danger', 'data-testid': 'modal-confirm' }, + }, + }, + data() { + return { + deleteUserList: null, + }; + }, + computed: { + deleteListName() { + return this.deleteUserList?.name; + }, + modalTitle() { + return sprintf(this.$options.translations.deleteListTitle, { + name: this.deleteListName, + }); + }, + }, + methods: { + createdTimeago(list) { + return sprintf(this.$options.translations.createdTimeagoLabel, { + timeago: this.timeFormatted(list.created_at), + }); + }, + displayList(list) { + return list.user_xids.replace(/,/g, ', '); + }, + onDelete() { + this.$emit('delete', this.deleteUserList); + }, + confirmDeleteList(list) { + this.deleteUserList = list; + }, + }, +}; +</script> +<template> + <div> + <div + v-for="list in userLists" + :key="list.id" + data-testid="ffUserList" + class="gl-border-b-solid gl-border-gray-100 gl-border-b-1 gl-w-full gl-py-4 gl-display-flex gl-justify-content-space-between" + > + <div class="gl-display-flex gl-flex-direction-column gl-overflow-hidden gl-flex-grow-1"> + <span data-testid="ffUserListName" class="gl-font-weight-bold gl-mb-2"> + {{ list.name }} + </span> + <span + v-gl-tooltip + :title="tooltipTitle(list.created_at)" + data-testid="ffUserListTimestamp" + class="gl-text-gray-300 gl-mb-2" + > + {{ createdTimeago(list) }} + </span> + <span data-testid="ffUserListIds" class="gl-str-truncated">{{ displayList(list) }}</span> + </div> + + <gl-button-group class="gl-align-self-start gl-mt-2"> + <gl-button + :href="list.path" + category="secondary" + icon="pencil" + :aria-label="$options.translations.editUserListLabel" + data-testid="edit-user-list" + /> + <gl-button + v-gl-modal="$options.modal.id" + category="secondary" + variant="danger" + icon="remove" + :aria-label="$options.modal.actionPrimary.text" + data-testid="delete-user-list" + @click="confirmDeleteList(list)" + /> + </gl-button-group> + </div> + <gl-modal + :title="modalTitle" + :modal-id="$options.modal.id" + :action-primary="$options.modal.actionPrimary" + static + @primary="onDelete" + > + <gl-sprintf :message="$options.translations.deleteListMessage"> + <template #name> + <b>{{ deleteListName }}</b> + </template> + </gl-sprintf> + </gl-modal> + </div> +</template> diff --git a/app/assets/javascripts/user_lists/store/index/actions.js b/app/assets/javascripts/user_lists/store/index/actions.js new file mode 100644 index 00000000000..432c576694a --- /dev/null +++ b/app/assets/javascripts/user_lists/store/index/actions.js @@ -0,0 +1,38 @@ +import Api from '~/api'; +import * as types from './mutation_types'; + +export const setUserListsOptions = ({ commit }, options) => + commit(types.SET_USER_LISTS_OPTIONS, options); + +export const fetchUserLists = ({ state, dispatch }) => { + dispatch('requestUserLists'); + + return Api.fetchFeatureFlagUserLists(state.projectId, state.options.page) + .then(({ data, headers }) => dispatch('receiveUserListsSuccess', { data, headers })) + .catch(() => dispatch('receiveUserListsError')); +}; + +export const requestUserLists = ({ commit }) => commit(types.REQUEST_USER_LISTS); +export const receiveUserListsSuccess = ({ commit }, response) => + commit(types.RECEIVE_USER_LISTS_SUCCESS, response); +export const receiveUserListsError = ({ commit }) => commit(types.RECEIVE_USER_LISTS_ERROR); + +export const deleteUserList = ({ state, dispatch }, list) => { + dispatch('requestDeleteUserList', list); + + return Api.deleteFeatureFlagUserList(state.projectId, list.iid) + .then(() => dispatch('fetchUserLists')) + .catch((error) => + dispatch('receiveDeleteUserListError', { + list, + error: error?.response?.data ?? error, + }), + ); +}; + +export const requestDeleteUserList = ({ commit }, list) => + commit(types.REQUEST_DELETE_USER_LIST, list); + +export const receiveDeleteUserListError = ({ commit }, { error, list }) => + commit(types.RECEIVE_DELETE_USER_LIST_ERROR, { error, list }); +export const clearAlert = ({ commit }, index) => commit(types.RECEIVE_CLEAR_ALERT, index); diff --git a/app/assets/javascripts/user_lists/store/index/index.js b/app/assets/javascripts/user_lists/store/index/index.js new file mode 100644 index 00000000000..9b9df59ed32 --- /dev/null +++ b/app/assets/javascripts/user_lists/store/index/index.js @@ -0,0 +1,11 @@ +import Vuex from 'vuex'; +import * as actions from './actions'; +import mutations from './mutations'; +import createState from './state'; + +export default (initialState) => + new Vuex.Store({ + actions, + mutations, + state: createState(initialState), + }); diff --git a/app/assets/javascripts/user_lists/store/index/mutation_types.js b/app/assets/javascripts/user_lists/store/index/mutation_types.js new file mode 100644 index 00000000000..5637ed60b7b --- /dev/null +++ b/app/assets/javascripts/user_lists/store/index/mutation_types.js @@ -0,0 +1,10 @@ +export const SET_USER_LISTS_OPTIONS = 'SET_FEATURE_FLAGS_OPTIONS'; + +export const REQUEST_USER_LISTS = 'REQUEST_USER_LISTS'; +export const RECEIVE_USER_LISTS_SUCCESS = 'RECEIVE_USER_LISTS_SUCCESS'; +export const RECEIVE_USER_LISTS_ERROR = 'RECEIVE_USER_LISTS_ERROR'; + +export const REQUEST_DELETE_USER_LIST = 'REQUEST_DELETE_USER_LIST'; +export const RECEIVE_DELETE_USER_LIST_ERROR = 'RECEIVE_DELETE_USER_LIST_ERROR'; + +export const RECEIVE_CLEAR_ALERT = 'RECEIVE_CLEAR_ALERT'; diff --git a/app/assets/javascripts/user_lists/store/index/mutations.js b/app/assets/javascripts/user_lists/store/index/mutations.js new file mode 100644 index 00000000000..8e2865dc165 --- /dev/null +++ b/app/assets/javascripts/user_lists/store/index/mutations.js @@ -0,0 +1,37 @@ +import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; +import * as types from './mutation_types'; + +export default { + [types.SET_USER_LISTS_OPTIONS](state, options = {}) { + state.options = options; + }, + [types.REQUEST_USER_LISTS](state) { + state.isLoading = true; + }, + [types.RECEIVE_USER_LISTS_SUCCESS](state, { data, headers }) { + state.isLoading = false; + state.hasError = false; + state.userLists = data || []; + + const normalizedHeaders = normalizeHeaders(headers); + const paginationInfo = parseIntPagination(normalizedHeaders); + state.count = paginationInfo?.total ?? state.userLists.length; + state.pageInfo = paginationInfo; + }, + [types.RECEIVE_USER_LISTS_ERROR](state) { + state.isLoading = false; + state.hasError = true; + }, + [types.REQUEST_DELETE_USER_LIST](state, list) { + state.userLists = state.userLists.filter((l) => l !== list); + }, + [types.RECEIVE_DELETE_USER_LIST_ERROR](state, { error, list }) { + state.isLoading = false; + state.hasError = false; + state.alerts = [].concat(error.message); + state.userLists = state.userLists.concat(list).sort((l1, l2) => l1.iid - l2.iid); + }, + [types.RECEIVE_CLEAR_ALERT](state, index) { + state.alerts.splice(index, 1); + }, +}; diff --git a/app/assets/javascripts/user_lists/store/index/state.js b/app/assets/javascripts/user_lists/store/index/state.js new file mode 100644 index 00000000000..0658d23cffc --- /dev/null +++ b/app/assets/javascripts/user_lists/store/index/state.js @@ -0,0 +1,10 @@ +export default ({ projectId }) => ({ + userLists: [], + alerts: [], + count: 0, + pageInfo: {}, + isLoading: true, + hasError: false, + options: {}, + projectId, +}); |