diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-06 15:09:14 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-06 15:09:14 +0000 |
commit | a268b09416c8dc3da3af38933028fa26375b88e0 (patch) | |
tree | 8f10484408a40e386b79f8bb3c2f4095dded85f7 /app | |
parent | 4ff56b118438f4fa6191b691fd968c75d8e94d5a (diff) | |
download | gitlab-ce-a268b09416c8dc3da3af38933028fa26375b88e0.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
14 files changed, 287 insertions, 358 deletions
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/app.vue b/app/assets/javascripts/analytics/instance_statistics/components/app.vue index abe9b45ed79..8df4d2e2524 100644 --- a/app/assets/javascripts/analytics/instance_statistics/components/app.vue +++ b/app/assets/javascripts/analytics/instance_statistics/components/app.vue @@ -1,32 +1,11 @@ <script> -import { s__ } from '~/locale'; import InstanceCounts from './instance_counts.vue'; import InstanceStatisticsCountChart from './instance_statistics_count_chart.vue'; import UsersChart from './users_chart.vue'; -import pipelinesStatsQuery from '../graphql/queries/pipeline_stats.query.graphql'; -import issuesAndMergeRequestsQuery from '../graphql/queries/issues_and_merge_requests.query.graphql'; import ProjectsAndGroupsChart from './projects_and_groups_chart.vue'; +import ChartsConfig from './charts_config'; import { TODAY, TOTAL_DAYS_TO_SHOW, START_DATE } from '../constants'; -const PIPELINES_KEY_TO_NAME_MAP = { - total: s__('InstanceAnalytics|Total'), - succeeded: s__('InstanceAnalytics|Succeeded'), - failed: s__('InstanceAnalytics|Failed'), - canceled: s__('InstanceAnalytics|Canceled'), - skipped: s__('InstanceAnalytics|Skipped'), -}; -const ISSUES_AND_MERGE_REQUESTS_KEY_TO_NAME_MAP = { - issues: s__('InstanceAnalytics|Issues'), - mergeRequests: s__('InstanceAnalytics|Merge Requests'), -}; -const loadPipelineChartError = s__( - 'InstanceAnalytics|Could not load the pipelines chart. Please refresh the page to try again.', -); -const loadIssuesAndMergeRequestsChartError = s__( - 'InstanceAnalytics|Could not load the issues and merge requests chart. Please refresh the page to try again.', -); -const noDataMessage = s__('InstanceAnalytics|There is no data available.'); - export default { name: 'InstanceStatisticsApp', components: { @@ -38,28 +17,7 @@ export default { TOTAL_DAYS_TO_SHOW, START_DATE, TODAY, - configs: [ - { - keyToNameMap: PIPELINES_KEY_TO_NAME_MAP, - prefix: 'pipelines', - loadChartError: loadPipelineChartError, - noDataMessage, - chartTitle: s__('InstanceAnalytics|Pipelines'), - yAxisTitle: s__('InstanceAnalytics|Items'), - xAxisTitle: s__('InstanceAnalytics|Month'), - query: pipelinesStatsQuery, - }, - { - keyToNameMap: ISSUES_AND_MERGE_REQUESTS_KEY_TO_NAME_MAP, - prefix: 'issuesAndMergeRequests', - loadChartError: loadIssuesAndMergeRequestsChartError, - noDataMessage, - chartTitle: s__('InstanceAnalytics|Issues & Merge Requests'), - yAxisTitle: s__('InstanceAnalytics|Items'), - xAxisTitle: s__('InstanceAnalytics|Month'), - query: issuesAndMergeRequestsQuery, - }, - ], + configs: ChartsConfig, }; </script> @@ -79,9 +37,7 @@ export default { <instance-statistics-count-chart v-for="chartOptions in $options.configs" :key="chartOptions.chartTitle" - :prefix="chartOptions.prefix" - :key-to-name-map="chartOptions.keyToNameMap" - :query="chartOptions.query" + :queries="chartOptions.queries" :x-axis-title="chartOptions.xAxisTitle" :y-axis-title="chartOptions.yAxisTitle" :load-chart-error-message="chartOptions.loadChartError" diff --git a/app/assets/javascripts/analytics/instance_statistics/components/charts_config.js b/app/assets/javascripts/analytics/instance_statistics/components/charts_config.js new file mode 100644 index 00000000000..6fba3c56cfe --- /dev/null +++ b/app/assets/javascripts/analytics/instance_statistics/components/charts_config.js @@ -0,0 +1,87 @@ +import { s__, __, sprintf } from '~/locale'; +import query from '../graphql/queries/instance_count.query.graphql'; + +const noDataMessage = s__('InstanceStatistics|No data available.'); + +export default [ + { + loadChartError: sprintf( + s__( + 'InstanceStatistics|Could not load the pipelines chart. Please refresh the page to try again.', + ), + ), + noDataMessage, + chartTitle: s__('InstanceStatistics|Pipelines'), + yAxisTitle: s__('InstanceStatistics|Items'), + xAxisTitle: s__('InstanceStatistics|Month'), + queries: [ + { + query, + title: s__('InstanceStatistics|Pipelines total'), + identifier: 'PIPELINES', + loadError: sprintf( + s__('InstanceStatistics|There was an error fetching the total pipelines'), + ), + }, + { + query, + title: s__('InstanceStatistics|Pipelines succeeded'), + identifier: 'PIPELINES_SUCCEEDED', + loadError: sprintf( + s__('InstanceStatistics|There was an error fetching the successful pipelines'), + ), + }, + { + query, + title: s__('InstanceStatistics|Pipelines failed'), + identifier: 'PIPELINES_FAILED', + loadError: sprintf( + s__('InstanceStatistics|There was an error fetching the failed pipelines'), + ), + }, + { + query, + title: s__('InstanceStatistics|Pipelines canceled'), + identifier: 'PIPELINES_CANCELED', + loadError: sprintf( + s__('InstanceStatistics|There was an error fetching the cancelled pipelines'), + ), + }, + { + query, + title: s__('InstanceStatistics|Pipelines skipped'), + identifier: 'PIPELINES_SKIPPED', + loadError: sprintf( + s__('InstanceStatistics|There was an error fetching the skipped pipelines'), + ), + }, + ], + }, + { + loadChartError: sprintf( + s__( + 'InstanceStatistics|Could not load the issues and merge requests chart. Please refresh the page to try again.', + ), + ), + noDataMessage, + chartTitle: s__('InstanceStatistics|Issues & Merge Requests'), + yAxisTitle: s__('InstanceStatistics|Items'), + xAxisTitle: s__('InstanceStatistics|Month'), + queries: [ + { + query, + title: __('Issues'), + identifier: 'ISSUES', + loadError: sprintf(s__('InstanceStatistics|There was an error fetching the issues')), + }, + { + query, + title: __('Merge requests'), + identifier: 'MERGE_REQUESTS', + loadError: sprintf( + s__('InstanceStatistics|There was an error fetching the merge requests'), + ), + }, + ], + }, +]; diff --git a/app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue b/app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue index 740af834618..a9bd1bb2f41 100644 --- a/app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue +++ b/app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue @@ -1,17 +1,19 @@ <script> import { GlLineChart } from '@gitlab/ui/dist/charts'; import { GlAlert } from '@gitlab/ui'; -import { mapValues, some, sum } from 'lodash'; +import { some, every } from 'lodash'; +import * as Sentry from '~/sentry/wrapper'; import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue'; import { differenceInMonths, formatDateAsMonth, getDayDifference, } from '~/lib/utils/datetime_utility'; -import { convertToTitleCase } from '~/lib/utils/text_utility'; -import { getAverageByMonth, sortByDate, extractValues } from '../utils'; +import { getAverageByMonth, getEarliestDate, generateDataKeys } from '../utils'; import { TODAY, START_DATE } from '../constants'; +const QUERY_DATA_KEY = 'instanceStatisticsMeasurements'; + export default { name: 'InstanceStatisticsCountChart', components: { @@ -21,18 +23,7 @@ export default { }, startDate: START_DATE, endDate: TODAY, - dataKey: 'nodes', - pageInfoKey: 'pageInfo', - firstKey: 'first', props: { - prefix: { - type: String, - required: true, - }, - keyToNameMap: { - type: Object, - required: true, - }, chartTitle: { type: String, required: true, @@ -53,112 +44,46 @@ export default { type: String, required: true, }, - query: { - type: Object, + queries: { + type: Array, required: true, }, }, data() { return { - loading: true, - loadingError: null, + errors: { ...generateDataKeys(this.queries, '') }, + ...generateDataKeys(this.queries, []), }; }, - apollo: { - pipelineStats: { - query() { - return this.query; - }, - variables() { - return this.nameKeys.reduce((memo, key) => { - const firstKey = `${this.$options.firstKey}${convertToTitleCase(key)}`; - return { ...memo, [firstKey]: this.totalDaysToShow }; - }, {}); - }, - update(data) { - const allData = extractValues(data, this.nameKeys, this.prefix, this.$options.dataKey); - const allPageInfo = extractValues( - data, - this.nameKeys, - this.prefix, - this.$options.pageInfoKey, - ); - - return { - ...mapValues(allData, sortByDate), - ...allPageInfo, - }; - }, - result() { - if (this.hasNextPage) { - this.fetchNextPage(); - } - }, - error() { - this.handleError(); - }, - }, - }, computed: { - nameKeys() { - return Object.keys(this.keyToNameMap); + errorMessages() { + return Object.values(this.errors); }, isLoading() { - return this.$apollo.queries.pipelineStats.loading; + return some(this.$apollo.queries, query => query?.loading); }, - totalDaysToShow() { - return getDayDifference(this.$options.startDate, this.$options.endDate); + allQueriesFailed() { + return every(this.errorMessages, message => message.length); }, - firstVariables() { - const firstDataPoints = extractValues( - this.pipelineStats, - this.nameKeys, - this.$options.dataKey, - '[0].recordedAt', - { renameKey: this.$options.firstKey }, - ); - - return Object.keys(firstDataPoints).reduce((memo, name) => { - const recordedAt = firstDataPoints[name]; - if (!recordedAt) { - return { ...memo, [name]: 0 }; - } - - const numberOfDays = Math.max( - 0, - getDayDifference(this.$options.startDate, new Date(recordedAt)), - ); - - return { ...memo, [name]: numberOfDays }; - }, {}); - }, - cursorVariables() { - return extractValues( - this.pipelineStats, - this.nameKeys, - this.$options.pageInfoKey, - 'endCursor', - ); - }, - hasNextPage() { - return ( - sum(Object.values(this.firstVariables)) > 0 && - some(this.pipelineStats, ({ hasNextPage }) => hasNextPage) - ); + hasLoadingErrors() { + return some(this.errorMessages, message => message.length); + }, + errorMessage() { + // show the generic loading message if all requests fail + return this.allQueriesFailed ? this.loadChartErrorMessage : this.errorMessages.join('\n\n'); }, hasEmptyDataSet() { return this.chartData.every(({ data }) => data.length === 0); }, + totalDaysToShow() { + return getDayDifference(this.$options.startDate, this.$options.endDate); + }, chartData() { const options = { shouldRound: true }; - - return this.nameKeys.map(key => { - const dataKey = `${this.$options.dataKey}${convertToTitleCase(key)}`; - return { - name: this.keyToNameMap[key], - data: getAverageByMonth(this.pipelineStats?.[dataKey], options), - }; - }); + return this.queries.map(({ identifier, title }) => ({ + name: title, + data: getAverageByMonth(this[identifier]?.nodes, options), + })); }, range() { return { @@ -188,26 +113,73 @@ export default { }; }, }, + created() { + this.queries.forEach(({ query, identifier, loadError }) => { + this.$apollo.addSmartQuery(identifier, { + query, + variables() { + return { + identifier, + first: this.totalDaysToShow, + after: null, + }; + }, + update(data) { + const { nodes = [], pageInfo } = data[QUERY_DATA_KEY] || {}; + return { + nodes, + pageInfo, + }; + }, + result() { + const { pageInfo, nodes } = this[identifier]; + if (pageInfo?.hasNextPage && this.calculateDaysToFetch(getEarliestDate(nodes)) > 0) { + this.fetchNextPage({ + query: this.$apollo.queries[identifier], + errorMessage: loadError, + pageInfo, + identifier, + }); + } + }, + error(error) { + this.handleError({ + message: loadError, + identifier, + error, + }); + }, + }); + }); + }, methods: { - handleError() { + calculateDaysToFetch(firstDataPointDate = null) { + return firstDataPointDate + ? Math.max(0, getDayDifference(this.$options.startDate, new Date(firstDataPointDate))) + : 0; + }, + handleError({ identifier, error, message }) { this.loadingError = true; + this.errors = { ...this.errors, [identifier]: message }; + Sentry.captureException(error); }, - fetchNextPage() { - this.$apollo.queries.pipelineStats + fetchNextPage({ query, pageInfo, identifier, errorMessage }) { + query .fetchMore({ variables: { - ...this.firstVariables, - ...this.cursorVariables, + identifier, + first: this.calculateDaysToFetch(getEarliestDate(this[identifier].nodes)), + after: pageInfo.endCursor, }, updateQuery: (previousResult, { fetchMoreResult }) => { - return Object.keys(fetchMoreResult).reduce((memo, key) => { - const { nodes, ...rest } = fetchMoreResult[key]; - const previousNodes = previousResult[key].nodes; - return { ...memo, [key]: { ...rest, nodes: [...previousNodes, ...nodes] } }; - }, {}); + const { nodes, ...rest } = fetchMoreResult[QUERY_DATA_KEY]; + const { nodes: previousNodes } = previousResult[QUERY_DATA_KEY]; + return { + [QUERY_DATA_KEY]: { ...rest, nodes: [...previousNodes, ...nodes] }, + }; }, }) - .catch(this.handleError); + .catch(error => this.handleError({ identifier, error, message: errorMessage })); }, }, }; @@ -215,13 +187,20 @@ export default { <template> <div> <h3>{{ chartTitle }}</h3> - <gl-alert v-if="loadingError" variant="danger" :dismissible="false" class="gl-mt-3"> - {{ loadChartErrorMessage }} - </gl-alert> - <chart-skeleton-loader v-else-if="isLoading" /> - <gl-alert v-else-if="hasEmptyDataSet" variant="info" :dismissible="false" class="gl-mt-3"> - {{ noDataMessage }} + <gl-alert v-if="hasLoadingErrors" variant="danger" :dismissible="false" class="gl-mt-3"> + {{ errorMessage }} </gl-alert> - <gl-line-chart v-else :option="chartOptions" :include-legend-avg-max="true" :data="chartData" /> + <div v-if="!allQueriesFailed"> + <chart-skeleton-loader v-if="isLoading" /> + <gl-alert v-else-if="hasEmptyDataSet" variant="info" :dismissible="false" class="gl-mt-3"> + {{ noDataMessage }} + </gl-alert> + <gl-line-chart + v-else + :option="chartOptions" + :include-legend-avg-max="true" + :data="chartData" + /> + </div> </div> </template> diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/instance_count.query.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/instance_count.query.graphql new file mode 100644 index 00000000000..dd22a16cd51 --- /dev/null +++ b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/instance_count.query.graphql @@ -0,0 +1,13 @@ +#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" +#import "../fragments/count.fragment.graphql" + +query getCount($identifier: MeasurementIdentifier!, $first: Int, $after: String) { + instanceStatisticsMeasurements(identifier: $identifier, first: $first, after: $after) { + nodes { + ...Count + } + pageInfo { + ...PageInfo + } + } +} diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/issues_and_merge_requests.query.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/issues_and_merge_requests.query.graphql deleted file mode 100644 index 96f21403b34..00000000000 --- a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/issues_and_merge_requests.query.graphql +++ /dev/null @@ -1,34 +0,0 @@ -#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" -#import "./count.fragment.graphql" - -query issuesAndMergeRequests( - $firstIssues: Int - $firstMergeRequests: Int - $endCursorIssues: String - $endCursorMergeRequests: String -) { - issuesAndMergeRequestsIssues: instanceStatisticsMeasurements( - identifier: ISSUES - first: $firstIssues - after: $endCursorIssues - ) { - nodes { - ...Count - } - pageInfo { - ...PageInfo - } - } - issuesAndMergeRequestsMergeRequests: instanceStatisticsMeasurements( - identifier: MERGE_REQUESTS - first: $firstMergeRequests - after: $endCursorMergeRequests - ) { - nodes { - ...Count - } - pageInfo { - ...PageInfo - } - } -} diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql deleted file mode 100644 index 3bf40403f91..00000000000 --- a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql +++ /dev/null @@ -1,76 +0,0 @@ -#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" -#import "./count.fragment.graphql" - -query pipelineStats( - $firstTotal: Int - $firstSucceeded: Int - $firstFailed: Int - $firstCanceled: Int - $firstSkipped: Int - $endCursorTotal: String - $endCursorSucceeded: String - $endCursorFailed: String - $endCursorCanceled: String - $endCursorSkipped: String -) { - pipelinesTotal: instanceStatisticsMeasurements( - identifier: PIPELINES - first: $firstTotal - after: $endCursorTotal - ) { - nodes { - ...Count - } - pageInfo { - ...PageInfo - } - } - pipelinesSucceeded: instanceStatisticsMeasurements( - identifier: PIPELINES_SUCCEEDED - first: $firstSucceeded - after: $endCursorSucceeded - ) { - nodes { - ...Count - } - pageInfo { - ...PageInfo - } - } - pipelinesFailed: instanceStatisticsMeasurements( - identifier: PIPELINES_FAILED - first: $firstFailed - after: $endCursorFailed - ) { - nodes { - ...Count - } - pageInfo { - ...PageInfo - } - } - pipelinesCanceled: instanceStatisticsMeasurements( - identifier: PIPELINES_CANCELED - first: $firstCanceled - after: $endCursorCanceled - ) { - nodes { - ...Count - } - pageInfo { - ...PageInfo - } - } - pipelinesSkipped: instanceStatisticsMeasurements( - identifier: PIPELINES_SKIPPED - first: $firstSkipped - after: $endCursorSkipped - ) { - nodes { - ...Count - } - pageInfo { - ...PageInfo - } - } -} diff --git a/app/assets/javascripts/analytics/instance_statistics/utils.js b/app/assets/javascripts/analytics/instance_statistics/utils.js index eef66165945..e1fa5d155a2 100644 --- a/app/assets/javascripts/analytics/instance_statistics/utils.js +++ b/app/assets/javascripts/analytics/instance_statistics/utils.js @@ -1,7 +1,6 @@ import { masks } from 'dateformat'; -import { get, sortBy } from 'lodash'; +import { get } from 'lodash'; import { formatDate } from '~/lib/utils/datetime_utility'; -import { convertToTitleCase } from '~/lib/utils/text_utility'; const { isoDate } = masks; @@ -42,38 +41,28 @@ export function getAverageByMonth(items = [], options = {}) { } /** - * Extracts values given a data set and a set of keys - * @example - * const data = { fooBar: { baz: 'quis' }, ignored: 'ignored' }; - * extractValues(data, ['fooBar'], 'foo', 'baz') => { bazBar: 'quis' } - * @param {Object} data set to extract values from - * @param {Array} nameKeys keys describing where to look for values in the data set - * @param {String} dataPrefix prefix to `nameKey` on where to get the data - * @param {String} nestedKey key nested in the data set to be extracted, - * this is also used to rename the newly created data set - * @param {Object} options - * @param {String} options.renameKey? optional rename key, if not provided nestedKey will be used - * @return {Object} the newly created data set with the extracted values + * Takes an array of instance counts and returns the last item in the list + * @param {Array} arr array of instance counts in the form { count: Number, recordedAt: date String } + * @return {String} the 'recordedAt' value of the earliest item */ -export function extractValues(data, nameKeys = [], dataPrefix, nestedKey, options = {}) { - const { renameKey = nestedKey } = options; - - return nameKeys.reduce((memo, name) => { - const titelCaseName = convertToTitleCase(name); - const dataKey = `${dataPrefix}${titelCaseName}`; - const newKey = `${renameKey}${titelCaseName}`; - const itemData = get(data[dataKey], nestedKey); - - return { ...memo, [newKey]: itemData }; - }, {}); -} +export const getEarliestDate = (arr = []) => { + const len = arr.length; + return get(arr, `[${len - 1}].recordedAt`, null); +}; /** - * Creates a new array of items sorted by the date string of each item - * @param {Array} items [description] - * @param {String} items[0] date string - * @return {Array} the new sorted array. + * Takes an array of queries and produces an object with the query identifier as key + * and a supplied defaultValue as its value + * @param {Array} queries array of chart query configs, + * see ./analytics/instance_statistics/components/charts_config.js + * @param {any} defaultValue value to set each identifier to + * @return {Object} key value pair of the form { queryIdentifier: defaultValue } */ -export function sortByDate(items = []) { - return sortBy(items, ({ recordedAt }) => new Date(recordedAt).getTime()); -} +export const generateDataKeys = (queries, defaultValue) => + queries.reduce( + (acc, { identifier }) => ({ + ...acc, + [identifier]: defaultValue, + }), + {}, + ); diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 0e2f2cf9d27..522f1d16df2 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import { GlToast } from '@gitlab/ui'; +import UserCallout from '~/user_callout'; import { parseBoolean } from '~/lib/utils/common_utils'; import Translate from '../vue_shared/translate'; import GroupFilterableList from './groups_filterable_list'; @@ -17,6 +18,9 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => { const containerEl = document.getElementById(containerId); let dataEl; + // eslint-disable-next-line no-new + new UserCallout(); + // Don't do anything if element doesn't exist (No groups) // This is for when the user enters directly to the page via URL if (!containerEl) { diff --git a/app/assets/javascripts/packages/details/components/package_title.vue b/app/assets/javascripts/packages/details/components/package_title.vue index 2789be30818..6b7eeacb964 100644 --- a/app/assets/javascripts/packages/details/components/package_title.vue +++ b/app/assets/javascripts/packages/details/components/package_title.vue @@ -1,6 +1,8 @@ <script> +/* eslint-disable vue/v-slot-style */ import { mapState, mapGetters } from 'vuex'; -import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; +import { GlIcon, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui'; +import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; import PackageTags from '../../shared/components/package_tags.vue'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import timeagoMixin from '~/vue_shared/mixins/timeago'; @@ -16,11 +18,20 @@ export default { GlSprintf, PackageTags, MetadataItem, + GlBadge, }, directives: { GlTooltip: GlTooltipDirective, }, mixins: [timeagoMixin], + i18n: { + packageInfo: __('v%{version} published %{timeAgo}'), + }, + data() { + return { + isDesktop: true, + }; + }, computed: { ...mapState(['packageEntity', 'packageFiles']), ...mapGetters(['packageTypeDisplay', 'packagePipeline', 'packageIcon']), @@ -31,8 +42,13 @@ export default { return numberToHumanSize(this.packageFiles.reduce((acc, p) => acc + p.size, 0)); }, }, - i18n: { - packageInfo: __('v%{version} published %{timeAgo}'), + mounted() { + this.isDesktop = GlBreakpointInstance.isDesktop(); + }, + methods: { + dynamicSlotName(index) { + return `metadata-tag${index}`; + }, }, }; </script> @@ -75,10 +91,21 @@ export default { <metadata-item data-testid="package-ref" icon="branch" :text="packagePipeline.ref" /> </template> - <template v-if="hasTagsToDisplay" #metadata-tags> + <template v-if="isDesktop && hasTagsToDisplay" #metadata-tags> <package-tags :tag-display-limit="2" :tags="packageEntity.tags" hide-label /> </template> + <!-- we need to duplicate the package tags on mobile to ensure proper styling inside the flex wrap --> + <template + v-for="(tag, index) in packageEntity.tags" + v-else-if="hasTagsToDisplay" + v-slot:[dynamicSlotName(index)] + > + <gl-badge :key="index" class="gl-my-1" data-testid="tag-badge" variant="info" size="sm"> + {{ tag.name }} + </gl-badge> + </template> + <template #right-actions> <slot name="delete-button"></slot> </template> diff --git a/app/assets/javascripts/vue_shared/components/registry/title_area.vue b/app/assets/javascripts/vue_shared/components/registry/title_area.vue index 06b4309ad42..4d47a34c9a3 100644 --- a/app/assets/javascripts/vue_shared/components/registry/title_area.vue +++ b/app/assets/javascripts/vue_shared/components/registry/title_area.vue @@ -30,8 +30,13 @@ export default { metadataSlots: [], }; }, - mounted() { - this.metadataSlots = Object.keys(this.$slots).filter(k => k.startsWith('metadata-')); + async mounted() { + const METADATA_PREFIX = 'metadata-'; + this.metadataSlots = Object.keys(this.$slots).filter(k => k.startsWith(METADATA_PREFIX)); + + // we need to wait for next tick to ensure that dynamic names slots are picked up + await this.$nextTick(); + this.metadataSlots = Object.keys(this.$slots).filter(k => k.startsWith(METADATA_PREFIX)); }, }; </script> diff --git a/app/services/ci/destroy_expired_job_artifacts_service.rb b/app/services/ci/destroy_expired_job_artifacts_service.rb index 0c41bf9bb6b..6e7caba8545 100644 --- a/app/services/ci/destroy_expired_job_artifacts_service.rb +++ b/app/services/ci/destroy_expired_job_artifacts_service.rb @@ -4,24 +4,23 @@ module Ci class DestroyExpiredJobArtifactsService include ::Gitlab::ExclusiveLeaseHelpers include ::Gitlab::LoopHelpers + include ::Gitlab::Utils::StrongMemoize BATCH_SIZE = 100 LOOP_TIMEOUT = 5.minutes - LEGACY_LOOP_TIMEOUT = 45.minutes LOOP_LIMIT = 1000 EXCLUSIVE_LOCK_KEY = 'expired_job_artifacts:destroy:lock' - LOCK_TIMEOUT = 10.minutes - LEGACY_LOCK_TIMEOUT = 50.minutes + LOCK_TIMEOUT = 6.minutes ## # Destroy expired job artifacts on GitLab instance # - # This destroy process cannot run for more than 10 minutes. This is for + # This destroy process cannot run for more than 6 minutes. This is for # preventing multiple `ExpireBuildArtifactsWorker` CRON jobs run concurrently, - # which is scheduled at every hour. + # which is scheduled every 7 minutes. def execute - in_lock(EXCLUSIVE_LOCK_KEY, ttl: lock_timeout, retries: 1) do - loop_until(timeout: loop_timeout, limit: LOOP_LIMIT) do + in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do + loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do destroy_artifacts_batch end end @@ -42,30 +41,19 @@ module Ci return false if artifacts.empty? - if parallel_destroy? - parallel_destroy_batch(artifacts) - else - legacy_destroy_batch(artifacts) - destroy_related_records_for(artifacts) - end - + parallel_destroy_batch(artifacts) true end + # TODO: Make sure this can also be parallelized + # https://gitlab.com/gitlab-org/gitlab/-/issues/270973 def destroy_pipeline_artifacts_batch artifacts = Ci::PipelineArtifact.expired(BATCH_SIZE).to_a return false if artifacts.empty? - legacy_destroy_batch(artifacts) - true - end - - def parallel_destroy? - ::Feature.enabled?(:ci_delete_objects) - end - - def legacy_destroy_batch(artifacts) artifacts.each(&:destroy!) + + true end def parallel_destroy_batch(job_artifacts) @@ -77,6 +65,7 @@ module Ci # This is executed outside of the transaction because it depends on Redis update_statistics_for(job_artifacts) + destroyed_artifacts_counter.increment({}, job_artifacts.size) end # This method is implemented in EE and it must do only database work @@ -91,19 +80,12 @@ module Ci end end - def loop_timeout - if parallel_destroy? - LOOP_TIMEOUT - else - LEGACY_LOOP_TIMEOUT - end - end + def destroyed_artifacts_counter + strong_memoize(:destroyed_artifacts_counter) do + name = :destroyed_job_artifacts_count_total + comment = 'Counter of destroyed expired job artifacts' - def lock_timeout - if parallel_destroy? - LOCK_TIMEOUT - else - LEGACY_LOCK_TIMEOUT + ::Gitlab::Metrics.counter(name, comment) end end end diff --git a/app/views/explore/projects/_projects.html.haml b/app/views/explore/projects/_projects.html.haml index d819c4ea554..4275f76c046 100644 --- a/app/views/explore/projects/_projects.html.haml +++ b/app/views/explore/projects/_projects.html.haml @@ -1,2 +1 @@ -- is_explore_page = defined?(explore_page) && explore_page -= render 'shared/projects/list', projects: projects, user: current_user, explore_page: is_explore_page, pipeline_status: Feature.enabled?(:dashboard_pipeline_status, default_enabled: true) += render 'shared/projects/list', projects: projects, user: current_user, explore_page: true, pipeline_status: Feature.enabled?(:dashboard_pipeline_status, default_enabled: true) diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml index 153c90e534e..ed508fa2506 100644 --- a/app/views/explore/projects/trending.html.haml +++ b/app/views/explore/projects/trending.html.haml @@ -10,4 +10,4 @@ = render 'explore/head' = render 'explore/projects/nav' unless Feature.enabled?(:project_list_filter_bar) && current_user -= render 'projects', projects: @projects, explore_page: true += render 'projects', projects: @projects diff --git a/app/workers/ci/delete_objects_worker.rb b/app/workers/ci/delete_objects_worker.rb index 1a886d0efeb..d845ad61358 100644 --- a/app/workers/ci/delete_objects_worker.rb +++ b/app/workers/ci/delete_objects_worker.rb @@ -18,14 +18,12 @@ module Ci end def max_running_jobs - if ::Feature.enabled?(:ci_delete_objects_low_concurrency) - 2 - elsif ::Feature.enabled?(:ci_delete_objects_medium_concurrency) + if ::Feature.enabled?(:ci_delete_objects_medium_concurrency) 20 elsif ::Feature.enabled?(:ci_delete_objects_high_concurrency) 50 else - 0 + 2 end end |