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:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-06-16 21:25:58 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-06-16 21:25:58 +0300
commita5f4bba440d7f9ea47046a0a561d49adf0a1e6d4 (patch)
treefb69158581673816a8cd895f9d352dcb3c678b1e /app/assets/javascripts/releases
parentd16b2e8639e99961de6ddc93909f3bb5c1445ba1 (diff)
Add latest changes from gitlab-org/gitlab@14-0-stable-eev14.0.0-rc42
Diffstat (limited to 'app/assets/javascripts/releases')
-rw-r--r--app/assets/javascripts/releases/components/app_index_apollo_client.vue275
-rw-r--r--app/assets/javascripts/releases/components/releases_empty_state.vue44
-rw-r--r--app/assets/javascripts/releases/components/releases_pagination_apollo_client.vue37
-rw-r--r--app/assets/javascripts/releases/components/releases_sort.vue6
-rw-r--r--app/assets/javascripts/releases/components/releases_sort_apollo_client.vue91
-rw-r--r--app/assets/javascripts/releases/constants.js21
-rw-r--r--app/assets/javascripts/releases/mount_index.js34
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/actions.js14
-rw-r--r--app/assets/javascripts/releases/stores/modules/index/actions.js6
9 files changed, 516 insertions, 12 deletions
diff --git a/app/assets/javascripts/releases/components/app_index_apollo_client.vue b/app/assets/javascripts/releases/components/app_index_apollo_client.vue
new file mode 100644
index 00000000000..ea0aa409577
--- /dev/null
+++ b/app/assets/javascripts/releases/components/app_index_apollo_client.vue
@@ -0,0 +1,275 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { historyPushState, getParameterByName } from '~/lib/utils/common_utils';
+import { scrollUp } from '~/lib/utils/scroll_utils';
+import { setUrlParams } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
+import { PAGE_SIZE, DEFAULT_SORT } from '~/releases/constants';
+import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
+import { convertAllReleasesGraphQLResponse } from '~/releases/util';
+import ReleaseBlock from './release_block.vue';
+import ReleaseSkeletonLoader from './release_skeleton_loader.vue';
+import ReleasesEmptyState from './releases_empty_state.vue';
+import ReleasesPaginationApolloClient from './releases_pagination_apollo_client.vue';
+import ReleasesSortApolloClient from './releases_sort_apollo_client.vue';
+
+export default {
+ name: 'ReleasesIndexApolloClientApp',
+ components: {
+ GlButton,
+ ReleaseBlock,
+ ReleaseSkeletonLoader,
+ ReleasesEmptyState,
+ ReleasesPaginationApolloClient,
+ ReleasesSortApolloClient,
+ },
+ inject: {
+ projectPath: {
+ default: '',
+ },
+ newReleasePath: {
+ default: '',
+ },
+ },
+ apollo: {
+ /**
+ * The same query as `fullGraphqlResponse`, except that it limits its
+ * results to a single item. This causes this request to complete much more
+ * quickly than `fullGraphqlResponse`, which allows the page to show
+ * meaningful content to the user much earlier.
+ */
+ singleGraphqlResponse: {
+ query: allReleasesQuery,
+ // This trick only works when paginating _forward_.
+ // When paginating backwards, limiting the query to a single item loads
+ // the _last_ item in the page, which is not useful for our purposes.
+ skip() {
+ return !this.includeSingleQuery;
+ },
+ variables() {
+ return {
+ ...this.queryVariables,
+ first: 1,
+ };
+ },
+ update(data) {
+ return { data };
+ },
+ error() {
+ this.singleRequestError = true;
+ },
+ },
+ fullGraphqlResponse: {
+ query: allReleasesQuery,
+ variables() {
+ return this.queryVariables;
+ },
+ update(data) {
+ return { data };
+ },
+ error(error) {
+ this.fullRequestError = true;
+
+ createFlash({
+ message: this.$options.i18n.errorMessage,
+ captureError: true,
+ error,
+ });
+ },
+ },
+ },
+ data() {
+ return {
+ singleRequestError: false,
+ fullRequestError: false,
+ cursors: {
+ before: getParameterByName('before'),
+ after: getParameterByName('after'),
+ },
+ sort: DEFAULT_SORT,
+ };
+ },
+ computed: {
+ queryVariables() {
+ let paginationParams = { first: PAGE_SIZE };
+ if (this.cursors.after) {
+ paginationParams = {
+ after: this.cursors.after,
+ first: PAGE_SIZE,
+ };
+ } else if (this.cursors.before) {
+ paginationParams = {
+ before: this.cursors.before,
+ last: PAGE_SIZE,
+ };
+ }
+
+ return {
+ fullPath: this.projectPath,
+ ...paginationParams,
+ sort: this.sort,
+ };
+ },
+ /**
+ * @returns {Boolean} Whether or not to request/include
+ * the results of the single-item query
+ */
+ includeSingleQuery() {
+ return Boolean(!this.cursors.before || this.cursors.after);
+ },
+ isSingleRequestLoading() {
+ return this.$apollo.queries.singleGraphqlResponse.loading;
+ },
+ isFullRequestLoading() {
+ return this.$apollo.queries.fullGraphqlResponse.loading;
+ },
+ /**
+ * @returns {Boolean} `true` if the `singleGraphqlResponse`
+ * query has finished loading without errors
+ */
+ isSingleRequestLoaded() {
+ return Boolean(!this.isSingleRequestLoading && this.singleGraphqlResponse?.data.project);
+ },
+ /**
+ * @returns {Boolean} `true` if the `fullGraphqlResponse`
+ * query has finished loading without errors
+ */
+ isFullRequestLoaded() {
+ return Boolean(!this.isFullRequestLoading && this.fullGraphqlResponse?.data.project);
+ },
+ releases() {
+ if (this.isFullRequestLoaded) {
+ return convertAllReleasesGraphQLResponse(this.fullGraphqlResponse).data;
+ }
+
+ if (this.isSingleRequestLoaded && this.includeSingleQuery) {
+ return convertAllReleasesGraphQLResponse(this.singleGraphqlResponse).data;
+ }
+
+ return [];
+ },
+ pageInfo() {
+ if (!this.isFullRequestLoaded) {
+ return {
+ hasPreviousPage: false,
+ hasNextPage: false,
+ };
+ }
+
+ return this.fullGraphqlResponse.data.project.releases.pageInfo;
+ },
+ shouldRenderEmptyState() {
+ return this.isFullRequestLoaded && this.releases.length === 0;
+ },
+ shouldRenderLoadingIndicator() {
+ return (
+ (this.isSingleRequestLoading && !this.singleRequestError && !this.isFullRequestLoaded) ||
+ (this.isFullRequestLoading && !this.fullRequestError)
+ );
+ },
+ shouldRenderPagination() {
+ return this.isFullRequestLoaded && !this.shouldRenderEmptyState;
+ },
+ },
+ created() {
+ this.updateQueryParamsFromUrl();
+
+ window.addEventListener('popstate', this.updateQueryParamsFromUrl);
+ },
+ destroyed() {
+ window.removeEventListener('popstate', this.updateQueryParamsFromUrl);
+ },
+ methods: {
+ getReleaseKey(release, index) {
+ return [release.tagName, release.name, index].join('|');
+ },
+ updateQueryParamsFromUrl() {
+ this.cursors.before = getParameterByName('before');
+ this.cursors.after = getParameterByName('after');
+ },
+ onPaginationButtonPress() {
+ this.updateQueryParamsFromUrl();
+
+ // In some cases, Apollo Client is able to pull its results from the cache instead of making
+ // a new network request. In these cases, the page's content gets swapped out immediately without
+ // changing the page's scroll, leaving the user looking at the bottom of the new page.
+ // To make the experience consistent, regardless of how the data is sourced, we manually
+ // scroll to the top of the page every time a pagination button is pressed.
+ scrollUp();
+ },
+ onSortChanged(newSort) {
+ if (this.sort === newSort) {
+ return;
+ }
+
+ // Remove the "before" and "after" query parameters from the URL,
+ // effectively placing the user back on page 1 of the results.
+ // This prevents the frontend from requesting the results sorted
+ // by one field (e.g. `released_at`) while using a pagination cursor
+ // intended for a different field (e.g.) `created_at`).
+ // For more details, see the MR that introduced this change:
+ // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63434
+ historyPushState(
+ setUrlParams({
+ before: null,
+ after: null,
+ }),
+ );
+
+ this.updateQueryParamsFromUrl();
+
+ this.sort = newSort;
+ },
+ },
+ i18n: {
+ newRelease: __('New release'),
+ errorMessage: __('An error occurred while fetching the releases. Please try again.'),
+ },
+};
+</script>
+<template>
+ <div class="flex flex-column mt-2">
+ <div class="gl-align-self-end gl-mb-3">
+ <releases-sort-apollo-client :value="sort" class="gl-mr-2" @input="onSortChanged" />
+
+ <gl-button
+ v-if="newReleasePath"
+ :href="newReleasePath"
+ :aria-describedby="shouldRenderEmptyState && 'releases-description'"
+ category="primary"
+ variant="success"
+ >{{ $options.i18n.newRelease }}</gl-button
+ >
+ </div>
+
+ <releases-empty-state v-if="shouldRenderEmptyState" />
+
+ <release-block
+ v-for="(release, index) in releases"
+ :key="getReleaseKey(release, index)"
+ :release="release"
+ :class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }"
+ />
+
+ <release-skeleton-loader v-if="shouldRenderLoadingIndicator" />
+
+ <releases-pagination-apollo-client
+ v-if="shouldRenderPagination"
+ :page-info="pageInfo"
+ @prev="onPaginationButtonPress"
+ @next="onPaginationButtonPress"
+ />
+ </div>
+</template>
+<style>
+.linked-card::after {
+ width: 1px;
+ content: ' ';
+ border: 1px solid #e5e5e5;
+ height: 17px;
+ top: 100%;
+ position: absolute;
+ left: 32px;
+}
+</style>
diff --git a/app/assets/javascripts/releases/components/releases_empty_state.vue b/app/assets/javascripts/releases/components/releases_empty_state.vue
new file mode 100644
index 00000000000..800497c186a
--- /dev/null
+++ b/app/assets/javascripts/releases/components/releases_empty_state.vue
@@ -0,0 +1,44 @@
+<script>
+import { GlEmptyState, GlLink } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ name: 'ReleasesEmptyState',
+ components: {
+ GlEmptyState,
+ GlLink,
+ },
+ inject: {
+ documentationPath: {
+ default: '',
+ },
+ illustrationPath: {
+ default: '',
+ },
+ },
+ i18n: {
+ emptyStateTitle: __('Getting started with releases'),
+ emptyStateText: __(
+ "Releases are based on Git tags and mark specific points in a project's development history. They can contain information about the type of changes and can also deliver binaries, like compiled versions of your software.",
+ ),
+ releasesDocumentation: __('Releases documentation'),
+ moreInformation: __('More information'),
+ },
+};
+</script>
+<template>
+ <gl-empty-state :title="$options.i18n.emptyStateTitle" :svg-path="illustrationPath">
+ <template #description>
+ <span id="releases-description">
+ {{ $options.i18n.emptyStateText }}
+ <gl-link
+ :href="documentationPath"
+ :aria-label="$options.i18n.releasesDocumentation"
+ target="_blank"
+ >
+ {{ $options.i18n.moreInformation }}
+ </gl-link>
+ </span>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/releases/components/releases_pagination_apollo_client.vue b/app/assets/javascripts/releases/components/releases_pagination_apollo_client.vue
new file mode 100644
index 00000000000..73339677a4b
--- /dev/null
+++ b/app/assets/javascripts/releases/components/releases_pagination_apollo_client.vue
@@ -0,0 +1,37 @@
+<script>
+import { GlKeysetPagination } from '@gitlab/ui';
+import { isBoolean } from 'lodash';
+import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
+
+export default {
+ name: 'ReleasesPaginationApolloClient',
+ components: { GlKeysetPagination },
+ props: {
+ pageInfo: {
+ type: Object,
+ required: true,
+ validator: (info) => isBoolean(info.hasPreviousPage) && isBoolean(info.hasNextPage),
+ },
+ },
+ methods: {
+ onPrev(before) {
+ historyPushState(buildUrlWithCurrentLocation(`?before=${before}`));
+ },
+ onNext(after) {
+ historyPushState(buildUrlWithCurrentLocation(`?after=${after}`));
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-display-flex gl-justify-content-center">
+ <gl-keyset-pagination
+ v-bind="pageInfo"
+ :prev-text="__('Prev')"
+ :next-text="__('Next')"
+ v-on="$listeners"
+ @prev="onPrev($event)"
+ @next="onNext($event)"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/releases/components/releases_sort.vue b/app/assets/javascripts/releases/components/releases_sort.vue
index 4988904a2cd..d4210dad19c 100644
--- a/app/assets/javascripts/releases/components/releases_sort.vue
+++ b/app/assets/javascripts/releases/components/releases_sort.vue
@@ -1,7 +1,7 @@
<script>
import { GlSorting, GlSortingItem } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
-import { ASCENDING_ODER, DESCENDING_ORDER, SORT_OPTIONS } from '../constants';
+import { ASCENDING_ORDER, DESCENDING_ORDER, SORT_OPTIONS } from '../constants';
export default {
name: 'ReleasesSort',
@@ -22,13 +22,13 @@ export default {
return option.label;
},
isSortAscending() {
- return this.sort === ASCENDING_ODER;
+ return this.sort === ASCENDING_ORDER;
},
},
methods: {
...mapActions('index', ['setSorting']),
onDirectionChange() {
- const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ODER;
+ const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ORDER;
this.setSorting({ sort });
this.$emit('sort:changed');
},
diff --git a/app/assets/javascripts/releases/components/releases_sort_apollo_client.vue b/app/assets/javascripts/releases/components/releases_sort_apollo_client.vue
new file mode 100644
index 00000000000..7257b34bbf6
--- /dev/null
+++ b/app/assets/javascripts/releases/components/releases_sort_apollo_client.vue
@@ -0,0 +1,91 @@
+<script>
+import { GlSorting, GlSortingItem } from '@gitlab/ui';
+import {
+ ASCENDING_ORDER,
+ DESCENDING_ORDER,
+ SORT_OPTIONS,
+ RELEASED_AT,
+ CREATED_AT,
+ RELEASED_AT_ASC,
+ RELEASED_AT_DESC,
+ CREATED_ASC,
+ ALL_SORTS,
+ SORT_MAP,
+} from '../constants';
+
+export default {
+ name: 'ReleasesSortApolloclient',
+ components: {
+ GlSorting,
+ GlSortingItem,
+ },
+ props: {
+ value: {
+ type: String,
+ required: true,
+ validator: (sort) => ALL_SORTS.includes(sort),
+ },
+ },
+ computed: {
+ orderBy() {
+ if (this.value === RELEASED_AT_ASC || this.value === RELEASED_AT_DESC) {
+ return RELEASED_AT;
+ }
+
+ return CREATED_AT;
+ },
+ direction() {
+ if (this.value === RELEASED_AT_ASC || this.value === CREATED_ASC) {
+ return ASCENDING_ORDER;
+ }
+
+ return DESCENDING_ORDER;
+ },
+ sortOptions() {
+ return SORT_OPTIONS;
+ },
+ sortText() {
+ return this.sortOptions.find((s) => s.orderBy === this.orderBy).label;
+ },
+ isDirectionAscending() {
+ return this.direction === ASCENDING_ORDER;
+ },
+ },
+ methods: {
+ onDirectionChange() {
+ const direction = this.isDirectionAscending ? DESCENDING_ORDER : ASCENDING_ORDER;
+ this.emitInputEventIfChanged(this.orderBy, direction);
+ },
+ onSortItemClick(item) {
+ this.emitInputEventIfChanged(item.orderBy, this.direction);
+ },
+ isActiveSortItem(item) {
+ return this.orderBy === item.orderBy;
+ },
+ emitInputEventIfChanged(orderBy, direction) {
+ const newSort = SORT_MAP[orderBy][direction];
+ if (newSort !== this.value) {
+ this.$emit('input', SORT_MAP[orderBy][direction]);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-sorting
+ :text="sortText"
+ :is-ascending="isDirectionAscending"
+ data-testid="releases-sort"
+ @sortDirectionChange="onDirectionChange"
+ >
+ <gl-sorting-item
+ v-for="item of sortOptions"
+ :key="item.orderBy"
+ :active="isActiveSortItem(item)"
+ @click="onSortItemClick(item)"
+ >
+ {{ item.label }}
+ </gl-sorting-item>
+ </gl-sorting>
+</template>
diff --git a/app/assets/javascripts/releases/constants.js b/app/assets/javascripts/releases/constants.js
index f9653e0befa..4f862741e11 100644
--- a/app/assets/javascripts/releases/constants.js
+++ b/app/assets/javascripts/releases/constants.js
@@ -15,7 +15,7 @@ export const DEFAULT_ASSET_LINK_TYPE = ASSET_LINK_TYPE.OTHER;
export const PAGE_SIZE = 10;
-export const ASCENDING_ODER = 'asc';
+export const ASCENDING_ORDER = 'asc';
export const DESCENDING_ORDER = 'desc';
export const RELEASED_AT = 'released_at';
export const CREATED_AT = 'created_at';
@@ -30,3 +30,22 @@ export const SORT_OPTIONS = [
label: __('Created date'),
},
];
+
+export const RELEASED_AT_ASC = 'RELEASED_AT_ASC';
+export const RELEASED_AT_DESC = 'RELEASED_AT_DESC';
+export const CREATED_ASC = 'CREATED_ASC';
+export const CREATED_DESC = 'CREATED_DESC';
+export const ALL_SORTS = [RELEASED_AT_ASC, RELEASED_AT_DESC, CREATED_ASC, CREATED_DESC];
+
+export const SORT_MAP = {
+ [RELEASED_AT]: {
+ [ASCENDING_ORDER]: RELEASED_AT_ASC,
+ [DESCENDING_ORDER]: RELEASED_AT_DESC,
+ },
+ [CREATED_AT]: {
+ [ASCENDING_ORDER]: CREATED_ASC,
+ [DESCENDING_ORDER]: CREATED_DESC,
+ },
+};
+
+export const DEFAULT_SORT = RELEASED_AT_DESC;
diff --git a/app/assets/javascripts/releases/mount_index.js b/app/assets/javascripts/releases/mount_index.js
index bb21ec7c43f..59f6ebfc928 100644
--- a/app/assets/javascripts/releases/mount_index.js
+++ b/app/assets/javascripts/releases/mount_index.js
@@ -1,14 +1,44 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
+import createDefaultClient from '~/lib/graphql';
import ReleaseIndexApp from './components/app_index.vue';
+import ReleaseIndexApollopClientApp from './components/app_index_apollo_client.vue';
import createStore from './stores';
import createIndexModule from './stores/modules/index';
-Vue.use(Vuex);
-
export default () => {
const el = document.getElementById('js-releases-page');
+ if (window.gon?.features?.releasesIndexApolloClient) {
+ Vue.use(VueApollo);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(
+ {},
+ {
+ // This page attempts to decrease the perceived loading time
+ // by sending two requests: one request for the first item only (which
+ // completes relatively quickly), and one for all the items (which is slower).
+ // By default, Apollo Client batches these requests together, which defeats
+ // the purpose of making separate requests. So we explicitly
+ // disable batching on this page.
+ batchMax: 1,
+ assumeImmutableResults: true,
+ },
+ ),
+ });
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: { ...el.dataset },
+ render: (h) => h(ReleaseIndexApollopClientApp),
+ });
+ }
+
+ Vue.use(Vuex);
+
return new Vue({
el,
store: createStore({
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
index b312c2a7506..5955ec3352e 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
@@ -1,4 +1,4 @@
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import createReleaseMutation from '~/releases/graphql/mutations/create_release.mutation.graphql';
@@ -39,7 +39,9 @@ export const fetchRelease = async ({ commit, state }) => {
commit(types.RECEIVE_RELEASE_SUCCESS, release);
} catch (error) {
commit(types.RECEIVE_RELEASE_ERROR, error);
- createFlash(s__('Release|Something went wrong while getting the release details.'));
+ createFlash({
+ message: s__('Release|Something went wrong while getting the release details.'),
+ });
}
};
@@ -124,7 +126,9 @@ export const createRelease = async ({ commit, dispatch, state, getters }) => {
dispatch('receiveSaveReleaseSuccess', response.data.releaseCreate.release.links.selfUrl);
} catch (error) {
commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
- createFlash(s__('Release|Something went wrong while creating a new release.'));
+ createFlash({
+ message: s__('Release|Something went wrong while creating a new release.'),
+ });
}
};
@@ -214,6 +218,8 @@ export const updateRelease = async ({ commit, dispatch, state, getters }) => {
dispatch('receiveSaveReleaseSuccess', state.release._links.self);
} catch (error) {
commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
- createFlash(s__('Release|Something went wrong while saving the release details.'));
+ createFlash({
+ message: s__('Release|Something went wrong while saving the release details.'),
+ });
}
};
diff --git a/app/assets/javascripts/releases/stores/modules/index/actions.js b/app/assets/javascripts/releases/stores/modules/index/actions.js
index 00be25f089b..d3bb11cab30 100644
--- a/app/assets/javascripts/releases/stores/modules/index/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/index/actions.js
@@ -1,4 +1,4 @@
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import { PAGE_SIZE } from '~/releases/constants';
import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
@@ -57,7 +57,9 @@ export const fetchReleases = ({ dispatch, commit, state }, { before, after }) =>
export const receiveReleasesError = ({ commit }) => {
commit(types.RECEIVE_RELEASES_ERROR);
- createFlash(__('An error occurred while fetching the releases. Please try again.'));
+ createFlash({
+ message: __('An error occurred while fetching the releases. Please try again.'),
+ });
};
export const setSorting = ({ commit }, data) => commit(types.SET_SORTING, data);