diff options
Diffstat (limited to 'app/assets/javascripts/user_lists/components')
5 files changed, 435 insertions, 0 deletions
diff --git a/app/assets/javascripts/user_lists/components/add_user_modal.vue b/app/assets/javascripts/user_lists/components/add_user_modal.vue new file mode 100644 index 00000000000..a8dde1f681e --- /dev/null +++ b/app/assets/javascripts/user_lists/components/add_user_modal.vue @@ -0,0 +1,72 @@ +<script> +import { GlModal, GlFormGroup, GlFormTextarea } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { ADD_USER_MODAL_ID } from '../constants/show'; + +export default { + components: { + GlFormGroup, + GlFormTextarea, + GlModal, + }, + props: { + visible: { + type: Boolean, + required: false, + default: false, + }, + }, + modalOptions: { + actionPrimary: { + text: s__('UserLists|Add'), + attributes: [{ 'data-testid': 'confirm-add-user-ids' }], + }, + actionCancel: { + text: s__('UserLists|Cancel'), + attributes: [{ 'data-testid': 'cancel-add-user-ids' }], + }, + modalId: ADD_USER_MODAL_ID, + static: true, + }, + translations: { + title: s__('UserLists|Add users'), + description: s__( + 'UserLists|Enter a comma separated list of user IDs. These IDs should be the users of the system in which the feature flag is set, not GitLab IDs', + ), + userIdsLabel: s__('UserLists|User IDs'), + }, + data() { + return { + userIds: '', + }; + }, + methods: { + submitUsers() { + this.$emit('addUsers', this.userIds); + this.clearInput(); + }, + clearInput() { + this.userIds = ''; + }, + }, +}; +</script> +<template> + <gl-modal + v-bind="$options.modalOptions" + :visible="visible" + data-testid="add-users-modal" + @primary="submitUsers" + @canceled="clearInput" + > + <template #modal-title> + {{ $options.translations.title }} + </template> + <template #default> + <p data-testid="add-userids-description">{{ $options.translations.description }}</p> + <gl-form-group label-for="add-user-ids" :label="$options.translations.userIdsLabel"> + <gl-form-textarea id="add-user-ids" v-model="userIds" /> + </gl-form-group> + </template> + </gl-modal> +</template> diff --git a/app/assets/javascripts/user_lists/components/edit_user_list.vue b/app/assets/javascripts/user_lists/components/edit_user_list.vue new file mode 100644 index 00000000000..d56c3d61027 --- /dev/null +++ b/app/assets/javascripts/user_lists/components/edit_user_list.vue @@ -0,0 +1,74 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; +import { s__, sprintf } from '~/locale'; +import statuses from '../constants/edit'; +import UserListForm from './user_list_form.vue'; + +export default { + components: { + GlAlert, + GlLoadingIcon, + UserListForm, + }, + inject: ['userListsDocsPath'], + translations: { + saveButtonLabel: s__('UserLists|Save'), + }, + computed: { + ...mapState(['userList', 'status', 'errorMessage']), + title() { + return sprintf(s__('UserLists|Edit %{name}'), { name: this.userList?.name }); + }, + isLoading() { + return this.status === statuses.LOADING; + }, + isError() { + return this.status === statuses.ERROR; + }, + hasUserList() { + return Boolean(this.userList); + }, + }, + mounted() { + this.fetchUserList(); + }, + methods: { + ...mapActions(['fetchUserList', 'updateUserList', 'dismissErrorAlert']), + }, +}; +</script> +<template> + <div> + <gl-alert + v-if="isError" + :dismissible="hasUserList" + variant="danger" + @dismiss="dismissErrorAlert" + > + <ul class="gl-mb-0"> + <li v-for="(message, index) in errorMessage" :key="index"> + {{ message }} + </li> + </ul> + </gl-alert> + + <gl-loading-icon v-if="isLoading" size="xl" /> + + <template v-else-if="hasUserList"> + <h3 + data-testid="user-list-title" + class="gl-font-weight-bold gl-pb-5 gl-border-b-solid gl-border-gray-100 gl-border-1" + > + {{ title }} + </h3> + <user-list-form + :cancel-path="userList.path" + :save-button-label="$options.translations.saveButtonLabel" + :user-lists-docs-path="userListsDocsPath" + :user-list="userList" + @submit="updateUserList" + /> + </template> + </div> +</template> diff --git a/app/assets/javascripts/user_lists/components/new_user_list.vue b/app/assets/javascripts/user_lists/components/new_user_list.vue new file mode 100644 index 00000000000..522e077fb25 --- /dev/null +++ b/app/assets/javascripts/user_lists/components/new_user_list.vue @@ -0,0 +1,50 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import { GlAlert } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import UserListForm from './user_list_form.vue'; + +export default { + components: { + GlAlert, + UserListForm, + }, + inject: ['userListsDocsPath', 'featureFlagsPath'], + translations: { + pageTitle: s__('UserLists|New list'), + createButtonLabel: s__('UserLists|Create'), + }, + computed: { + ...mapState(['userList', 'errorMessage']), + isError() { + return Array.isArray(this.errorMessage) && this.errorMessage.length > 0; + }, + }, + methods: { + ...mapActions(['createUserList', 'dismissErrorAlert']), + }, +}; +</script> +<template> + <div> + <gl-alert v-if="isError" variant="danger" @dismiss="dismissErrorAlert"> + <ul class="gl-mb-0"> + <li v-for="(message, index) in errorMessage" :key="index"> + {{ message }} + </li> + </ul> + </gl-alert> + + <h3 class="gl-font-weight-bold gl-pb-5 gl-border-b-solid gl-border-gray-100 gl-border-1"> + {{ $options.translations.pageTitle }} + </h3> + + <user-list-form + :cancel-path="featureFlagsPath" + :save-button-label="$options.translations.createButtonLabel" + :user-lists-docs-path="userListsDocsPath" + :user-list="userList" + @submit="createUserList" + /> + </div> +</template> diff --git a/app/assets/javascripts/user_lists/components/user_list.vue b/app/assets/javascripts/user_lists/components/user_list.vue new file mode 100644 index 00000000000..0e2b72c1423 --- /dev/null +++ b/app/assets/javascripts/user_lists/components/user_list.vue @@ -0,0 +1,142 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import { + GlAlert, + GlButton, + GlEmptyState, + GlLoadingIcon, + GlModalDirective as GlModal, +} from '@gitlab/ui'; +import { s__, __ } from '~/locale'; +import { states, ADD_USER_MODAL_ID } from '../constants/show'; +import AddUserModal from './add_user_modal.vue'; + +const commonTableClasses = ['gl-py-5', 'gl-border-b-1', 'gl-border-b-solid', 'gl-border-gray-100']; + +export default { + components: { + GlAlert, + GlButton, + GlEmptyState, + GlLoadingIcon, + AddUserModal, + }, + directives: { + GlModal, + }, + props: { + emptyStatePath: { + required: true, + type: String, + }, + }, + translations: { + addUserButtonLabel: s__('UserLists|Add Users'), + emptyStateTitle: s__('UserLists|There are no users'), + emptyStateDescription: s__( + 'UserLists|Define a set of users to be used within feature flag strategies', + ), + userIdLabel: s__('UserLists|User IDs'), + userIdColumnHeader: s__('UserLists|User ID'), + errorMessage: __('Something went wrong on our end. Please try again!'), + editButtonLabel: s__('UserLists|Edit'), + }, + classes: { + headerClasses: [ + 'gl-display-flex', + 'gl-justify-content-space-between', + 'gl-pb-5', + 'gl-border-b-1', + 'gl-border-b-solid', + 'gl-border-gray-100', + ].join(' '), + tableHeaderClasses: commonTableClasses.join(' '), + tableRowClasses: [ + ...commonTableClasses, + 'gl-display-flex', + 'gl-justify-content-space-between', + 'gl-align-items-center', + ].join(' '), + }, + ADD_USER_MODAL_ID, + computed: { + ...mapState(['userList', 'userIds', 'state']), + name() { + return this.userList?.name ?? ''; + }, + hasUserIds() { + return this.userIds.length > 0; + }, + isLoading() { + return this.state === states.LOADING; + }, + hasError() { + return this.state === states.ERROR; + }, + editPath() { + return this.userList?.edit_path; + }, + }, + mounted() { + this.fetchUserList(); + }, + methods: { + ...mapActions(['fetchUserList', 'dismissErrorAlert', 'removeUserId', 'addUserIds']), + }, +}; +</script> +<template> + <div> + <gl-alert v-if="hasError" variant="danger" @dismiss="dismissErrorAlert"> + {{ $options.translations.errorMessage }} + </gl-alert> + <gl-loading-icon v-if="isLoading" size="xl" class="gl-mt-6" /> + <div v-else> + <add-user-modal @addUsers="addUserIds" /> + <div :class="$options.classes.headerClasses"> + <div> + <h3>{{ name }}</h3> + <h4 class="gl-text-gray-500">{{ $options.translations.userIdLabel }}</h4> + </div> + <div class="gl-mt-6"> + <gl-button v-if="editPath" :href="editPath" data-testid="edit-user-list" class="gl-mr-3"> + {{ $options.translations.editButtonLabel }} + </gl-button> + <gl-button + v-gl-modal="$options.ADD_USER_MODAL_ID" + data-testid="add-users" + variant="success" + > + {{ $options.translations.addUserButtonLabel }} + </gl-button> + </div> + </div> + <div v-if="hasUserIds"> + <div :class="$options.classes.tableHeaderClasses"> + {{ $options.translations.userIdColumnHeader }} + </div> + <div + v-for="id in userIds" + :key="id" + data-testid="user-id-row" + :class="$options.classes.tableRowClasses" + > + <span data-testid="user-id">{{ id }}</span> + <gl-button + category="secondary" + variant="danger" + icon="remove" + data-testid="delete-user-id" + @click="removeUserId(id)" + /> + </div> + </div> + <gl-empty-state + v-else + :title="$options.translations.emptyStateTitle" + :description="$options.translations.emptyStateDescription" + :svg-path="emptyStatePath" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/user_lists/components/user_list_form.vue b/app/assets/javascripts/user_lists/components/user_list_form.vue new file mode 100644 index 00000000000..657acb51fee --- /dev/null +++ b/app/assets/javascripts/user_lists/components/user_list_form.vue @@ -0,0 +1,97 @@ +<script> +import { GlButton, GlFormGroup, GlFormInput, GlLink, GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; + +export default { + components: { + GlButton, + GlFormGroup, + GlFormInput, + GlLink, + GlSprintf, + }, + props: { + cancelPath: { + type: String, + required: true, + }, + saveButtonLabel: { + type: String, + required: true, + }, + userListsDocsPath: { + type: String, + required: true, + }, + userList: { + type: Object, + required: true, + }, + }, + classes: { + actionContainer: [ + 'gl-py-5', + 'gl-display-flex', + 'gl-justify-content-space-between', + 'gl-px-4', + 'gl-border-t-solid', + 'gl-border-gray-100', + 'gl-border-1', + 'gl-bg-gray-10', + ], + }, + translations: { + formLabel: s__('UserLists|Feature flag list'), + formSubtitle: s__( + 'UserLists|Lists allow you to define a set of users to be used with feature flags. %{linkStart}Read more about feature flag lists.%{linkEnd}', + ), + nameLabel: s__('UserLists|Name'), + cancelButtonLabel: s__('UserLists|Cancel'), + }, + data() { + return { + name: this.userList.name, + }; + }, + methods: { + submit() { + this.$emit('submit', { ...this.userList, name: this.name }); + }, + }, +}; +</script> +<template> + <div> + <div class="gl-display-flex gl-mt-7"> + <div class="gl-flex-basis-0 gl-mr-7"> + <h4 class="gl-min-width-fit-content gl-white-space-nowrap"> + {{ $options.translations.formLabel }} + </h4> + <gl-sprintf :message="$options.translations.formSubtitle" class="gl-text-gray-500"> + <template #link="{ content }"> + <gl-link :href="userListsDocsPath" data-testid="user-list-docs-link"> + {{ content }} + </gl-link> + </template> + </gl-sprintf> + </div> + <div class="gl-flex-fill-1 gl-ml-7"> + <gl-form-group + label-for="user-list-name" + :label="$options.translations.nameLabel" + class="gl-mb-7" + > + <gl-form-input id="user-list-name" v-model="name" data-testid="user-list-name" required /> + </gl-form-group> + <div :class="$options.classes.actionContainer"> + <gl-button variant="success" data-testid="save-user-list" @click="submit"> + {{ saveButtonLabel }} + </gl-button> + <gl-button :href="cancelPath" data-testid="user-list-cancel"> + {{ $options.translations.cancelButtonLabel }} + </gl-button> + </div> + </div> + </div> + </div> +</template> |