summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-06 15:09:14 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-06 15:09:14 +0000
commita268b09416c8dc3da3af38933028fa26375b88e0 (patch)
tree8f10484408a40e386b79f8bb3c2f4095dded85f7 /app
parent4ff56b118438f4fa6191b691fd968c75d8e94d5a (diff)
downloadgitlab-ce-a268b09416c8dc3da3af38933028fa26375b88e0.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/app.vue50
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/charts_config.js87
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue219
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/graphql/queries/instance_count.query.graphql13
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/graphql/queries/issues_and_merge_requests.query.graphql34
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql76
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/utils.js55
-rw-r--r--app/assets/javascripts/groups/index.js4
-rw-r--r--app/assets/javascripts/packages/details/components/package_title.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/title_area.vue9
-rw-r--r--app/services/ci/destroy_expired_job_artifacts_service.rb52
-rw-r--r--app/views/explore/projects/_projects.html.haml3
-rw-r--r--app/views/explore/projects/trending.html.haml2
-rw-r--r--app/workers/ci/delete_objects_worker.rb6
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