diff options
Diffstat (limited to 'app/assets/javascripts/feature_flags/components/feature_flags.vue')
-rw-r--r-- | app/assets/javascripts/feature_flags/components/feature_flags.vue | 326 |
1 files changed, 326 insertions, 0 deletions
diff --git a/app/assets/javascripts/feature_flags/components/feature_flags.vue b/app/assets/javascripts/feature_flags/components/feature_flags.vue new file mode 100644 index 00000000000..eb7046a3d9b --- /dev/null +++ b/app/assets/javascripts/feature_flags/components/feature_flags.vue @@ -0,0 +1,326 @@ +<script> +import { mapState, mapActions } from 'vuex'; +import { isEmpty } from 'lodash'; +import { GlAlert, GlButton, GlModalDirective, GlSprintf, GlTabs } from '@gitlab/ui'; + +import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../constants'; +import FeatureFlagsTab from './feature_flags_tab.vue'; +import FeatureFlagsTable from './feature_flags_table.vue'; +import UserListsTable from './user_lists_table.vue'; +import { s__ } from '~/locale'; +import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; +import { + buildUrlWithCurrentLocation, + getParameterByName, + historyPushState, +} from '~/lib/utils/common_utils'; + +import ConfigureFeatureFlagsModal from './configure_feature_flags_modal.vue'; + +const SCOPES = { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE }; + +export default { + components: { + ConfigureFeatureFlagsModal, + FeatureFlagsTab, + FeatureFlagsTable, + GlAlert, + GlButton, + GlSprintf, + GlTabs, + TablePagination, + UserListsTable, + }, + directives: { + GlModal: GlModalDirective, + }, + inject: { + newUserListPath: { default: '' }, + newFeatureFlagPath: { default: '' }, + canUserConfigure: { required: true }, + featureFlagsLimitExceeded: { required: true }, + }, + data() { + const scope = getParameterByName('scope') || SCOPES.FEATURE_FLAG_SCOPE; + return { + scope, + page: getParameterByName('page') || '1', + isUserListAlertDismissed: false, + shouldShowFeatureFlagsLimitWarning: this.featureFlagsLimitExceeded, + selectedTab: Object.values(SCOPES).indexOf(scope), + }; + }, + computed: { + ...mapState([ + FEATURE_FLAG_SCOPE, + USER_LIST_SCOPE, + 'alerts', + 'count', + 'pageInfo', + 'isLoading', + 'hasError', + 'options', + 'instanceId', + 'isRotating', + 'hasRotateError', + ]), + topAreaBaseClasses() { + return ['gl-display-flex', 'gl-flex-direction-column']; + }, + canUserRotateToken() { + return this.rotateInstanceIdPath !== ''; + }, + currentlyDisplayedData() { + return this.dataForScope(this.scope); + }, + shouldRenderPagination() { + return ( + !this.isLoading && + !this.hasError && + this.currentlyDisplayedData.length > 0 && + this.pageInfo[this.scope].total > this.pageInfo[this.scope].perPage + ); + }, + shouldShowEmptyState() { + return !this.isLoading && !this.hasError && this.currentlyDisplayedData.length === 0; + }, + shouldRenderErrorState() { + return this.hasError && !this.isLoading; + }, + shouldRenderFeatureFlags() { + return this.shouldRenderTable(SCOPES.FEATURE_FLAG_SCOPE); + }, + shouldRenderUserLists() { + return this.shouldRenderTable(SCOPES.USER_LIST_SCOPE); + }, + hasNewPath() { + return !isEmpty(this.newFeatureFlagPath); + }, + emptyStateTitle() { + return s__('FeatureFlags|Get started with feature flags'); + }, + }, + created() { + this.setFeatureFlagsOptions({ scope: this.scope, page: this.page }); + this.fetchFeatureFlags(); + this.fetchUserLists(); + }, + methods: { + ...mapActions([ + 'setFeatureFlagsOptions', + 'fetchFeatureFlags', + 'fetchUserLists', + 'rotateInstanceId', + 'toggleFeatureFlag', + 'deleteUserList', + 'clearAlert', + ]), + onChangeTab(scope) { + this.scope = scope; + this.updateFeatureFlagOptions({ + scope, + page: '1', + }); + }, + onFeatureFlagsTab() { + this.onChangeTab(SCOPES.FEATURE_FLAG_SCOPE); + }, + onUserListsTab() { + this.onChangeTab(SCOPES.USER_LIST_SCOPE); + }, + onChangePage(page) { + this.updateFeatureFlagOptions({ + scope: this.scope, + /* URLS parameters are strings, we need to parse to match types */ + page: Number(page).toString(), + }); + }, + updateFeatureFlagOptions(parameters) { + const queryString = Object.keys(parameters) + .map(parameter => { + const value = parameters[parameter]; + return `${parameter}=${encodeURIComponent(value)}`; + }) + .join('&'); + + historyPushState(buildUrlWithCurrentLocation(`?${queryString}`)); + this.setFeatureFlagsOptions(parameters); + if (this.scope === SCOPES.FEATURE_FLAG_SCOPE) { + this.fetchFeatureFlags(); + } else { + this.fetchUserLists(); + } + }, + shouldRenderTable(scope) { + return ( + !this.isLoading && + this.dataForScope(scope).length > 0 && + !this.hasError && + this.scope === scope + ); + }, + dataForScope(scope) { + return this[scope]; + }, + onDismissFeatureFlagsLimitWarning() { + this.shouldShowFeatureFlagsLimitWarning = false; + }, + onNewFeatureFlagCLick() { + if (this.featureFlagsLimitExceeded) { + this.shouldShowFeatureFlagsLimitWarning = true; + } + }, + }, +}; +</script> +<template> + <div> + <gl-alert + v-if="shouldShowFeatureFlagsLimitWarning" + variant="warning" + @dismiss="onDismissFeatureFlagsLimitWarning" + > + <gl-sprintf + :message=" + s__( + 'FeatureFlags|Feature flags limit reached (%{featureFlagsLimit}). Delete one or more feature flags before adding new ones.', + ) + " + > + <template #featureFlagsLimit> + <span>{{ featureFlagsLimit }}</span> + </template> + </gl-sprintf> + </gl-alert> + <configure-feature-flags-modal + v-if="canUserConfigure" + :instance-id="instanceId" + :is-rotating="isRotating" + :has-rotate-error="hasRotateError" + :can-user-rotate-token="canUserRotateToken" + modal-id="configure-feature-flags" + @token="rotateInstanceId()" + /> + <div :class="topAreaBaseClasses"> + <div class="gl-display-flex gl-flex-direction-column gl-display-md-none!"> + <gl-button + v-if="canUserConfigure" + v-gl-modal="'configure-feature-flags'" + variant="info" + category="secondary" + data-qa-selector="configure_feature_flags_button" + data-testid="ff-configure-button" + class="gl-mb-3" + > + {{ s__('FeatureFlags|Configure') }} + </gl-button> + + <gl-button + v-if="newUserListPath" + :href="newUserListPath" + variant="success" + category="secondary" + class="gl-mb-3" + data-testid="ff-new-list-button" + > + {{ s__('FeatureFlags|New user list') }} + </gl-button> + + <gl-button + v-if="hasNewPath" + :href="featureFlagsLimitExceeded ? '' : newFeatureFlagPath" + variant="success" + data-testid="ff-new-button" + @click="onNewFeatureFlagCLick" + > + {{ s__('FeatureFlags|New feature flag') }} + </gl-button> + </div> + <gl-tabs v-model="selectedTab" class="gl-align-items-center gl-w-full"> + <feature-flags-tab + :title="s__('FeatureFlags|Feature Flags')" + :count="count.featureFlags" + :alerts="alerts" + :is-loading="isLoading" + :loading-label="s__('FeatureFlags|Loading feature flags')" + :error-state="shouldRenderErrorState" + :error-title="s__(`FeatureFlags|There was an error fetching the feature flags.`)" + :empty-state="shouldShowEmptyState" + :empty-title="emptyStateTitle" + data-testid="feature-flags-tab" + @dismissAlert="clearAlert" + @changeTab="onFeatureFlagsTab" + > + <feature-flags-table + v-if="shouldRenderFeatureFlags" + :feature-flags="featureFlags" + @toggle-flag="toggleFeatureFlag" + /> + </feature-flags-tab> + <feature-flags-tab + :title="s__('FeatureFlags|User Lists')" + :count="count.userLists" + :alerts="alerts" + :is-loading="isLoading" + :loading-label="s__('FeatureFlags|Loading user lists')" + :error-state="shouldRenderErrorState" + :error-title="s__(`FeatureFlags|There was an error fetching the user lists.`)" + :empty-state="shouldShowEmptyState" + :empty-title="emptyStateTitle" + data-testid="user-lists-tab" + @dismissAlert="clearAlert" + @changeTab="onUserListsTab" + > + <user-lists-table + v-if="shouldRenderUserLists" + :user-lists="userLists" + @delete="deleteUserList" + /> + </feature-flags-tab> + <template #tabs-end> + <div + class="gl-display-none gl-display-md-flex gl-align-items-center gl-flex-fill-1 gl-justify-content-end" + > + <gl-button + v-if="canUserConfigure" + v-gl-modal="'configure-feature-flags'" + variant="info" + category="secondary" + data-qa-selector="configure_feature_flags_button" + data-testid="ff-configure-button" + class="gl-mb-0 gl-mr-4" + > + {{ s__('FeatureFlags|Configure') }} + </gl-button> + + <gl-button + v-if="newUserListPath" + :href="newUserListPath" + variant="success" + category="secondary" + class="gl-mb-0 gl-mr-4" + data-testid="ff-new-list-button" + > + {{ s__('FeatureFlags|New user list') }} + </gl-button> + + <gl-button + v-if="hasNewPath" + :href="featureFlagsLimitExceeded ? '' : newFeatureFlagPath" + variant="success" + data-testid="ff-new-button" + @click="onNewFeatureFlagCLick" + > + {{ s__('FeatureFlags|New feature flag') }} + </gl-button> + </div> + </template> + </gl-tabs> + </div> + <table-pagination + v-if="shouldRenderPagination" + :change="onChangePage" + :page-info="pageInfo[scope]" + /> + </div> +</template> |