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>2022-01-28 12:15:22 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-01-28 12:15:22 +0300
commitfa4a0663e8c12b7b80e9e7edec99e6ca5a04a253 (patch)
tree9ed8293aabb57dd4e162498791a7a05e9de016b9
parent4315ed03878b5c5d4ab6a49bf41857cfe036dac7 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/runner/components/runner_type_tabs.vue59
-rw-r--r--app/assets/javascripts/runner/constants.js6
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue81
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage_event_model.rb40
-rw-r--r--app/views/groups/runners/index.html.haml3
-rw-r--r--lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb10
-rw-r--r--lib/gitlab/analytics/cycle_analytics/aggregated/data_collector.rb8
-rw-r--r--lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb27
-rw-r--r--lib/gitlab/analytics/cycle_analytics/data_collector.rb6
-rw-r--r--lib/tasks/gitlab/banzai.rake20
-rw-r--r--locale/gitlab.pot6
-rw-r--r--qa/qa/tools/delete_test_resources.rb87
-rw-r--r--spec/frontend/runner/components/runner_type_tabs_spec.js22
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js64
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb15
-rw-r--r--spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb18
-rw-r--r--spec/tooling/danger/project_helper_spec.rb1
-rw-r--r--tooling/danger/project_helper.rb2
18 files changed, 347 insertions, 128 deletions
diff --git a/app/assets/javascripts/runner/components/runner_type_tabs.vue b/app/assets/javascripts/runner/components/runner_type_tabs.vue
index b767dafaccf..25ed6600dc9 100644
--- a/app/assets/javascripts/runner/components/runner_type_tabs.vue
+++ b/app/assets/javascripts/runner/components/runner_type_tabs.vue
@@ -1,27 +1,21 @@
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
-import { s__ } from '~/locale';
import { searchValidator } from '~/runner/runner_search_utils';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
+import {
+ INSTANCE_TYPE,
+ GROUP_TYPE,
+ PROJECT_TYPE,
+ I18N_ALL_TYPES,
+ I18N_INSTANCE_TYPE,
+ I18N_GROUP_TYPE,
+ I18N_PROJECT_TYPE,
+} from '../constants';
-const tabs = [
- {
- title: s__('Runners|All'),
- runnerType: null,
- },
- {
- title: s__('Runners|Instance'),
- runnerType: INSTANCE_TYPE,
- },
- {
- title: s__('Runners|Group'),
- runnerType: GROUP_TYPE,
- },
- {
- title: s__('Runners|Project'),
- runnerType: PROJECT_TYPE,
- },
-];
+const I18N_TAB_TITLES = {
+ [INSTANCE_TYPE]: I18N_INSTANCE_TYPE,
+ [GROUP_TYPE]: I18N_GROUP_TYPE,
+ [PROJECT_TYPE]: I18N_PROJECT_TYPE,
+};
export default {
components: {
@@ -29,12 +23,34 @@ export default {
GlTab,
},
props: {
+ runnerTypes: {
+ type: Array,
+ required: false,
+ default: () => [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE],
+ },
value: {
type: Object,
required: true,
validator: searchValidator,
},
},
+ computed: {
+ tabs() {
+ const tabs = this.runnerTypes.map((runnerType) => ({
+ title: I18N_TAB_TITLES[runnerType],
+ runnerType,
+ }));
+
+ // Always add a "All" tab that resets filters
+ return [
+ {
+ title: I18N_ALL_TYPES,
+ runnerType: null,
+ },
+ ...tabs,
+ ];
+ },
+ },
methods: {
onTabSelected({ runnerType }) {
this.$emit('input', {
@@ -47,13 +63,12 @@ export default {
return runnerType === this.value.runnerType;
},
},
- tabs,
};
</script>
<template>
<gl-tabs v-bind="$attrs" data-testid="runner-type-tabs">
<gl-tab
- v-for="tab in $options.tabs"
+ v-for="tab in tabs"
:key="`${tab.runnerType}`"
:active="isTabActive(tab)"
@click="onTabSelected(tab)"
diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js
index ad0437d84ef..221696836ac 100644
--- a/app/assets/javascripts/runner/constants.js
+++ b/app/assets/javascripts/runner/constants.js
@@ -2,12 +2,16 @@ import { __, s__ } from '~/locale';
export const RUNNER_PAGE_SIZE = 20;
export const RUNNER_JOB_COUNT_LIMIT = 1000;
-export const GROUP_RUNNER_COUNT_LIMIT = 1000;
export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.');
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
// Type
+
+export const I18N_ALL_TYPES = s__('Runners|All');
+export const I18N_INSTANCE_TYPE = s__('Runners|Instance');
+export const I18N_GROUP_TYPE = s__('Runners|Group');
+export const I18N_PROJECT_TYPE = s__('Runners|Project');
export const I18N_INSTANCE_RUNNER_DESCRIPTION = s__('Runners|Available to all projects');
export const I18N_GROUP_RUNNER_DESCRIPTION = s__(
'Runners|Available to all projects and subgroups in the group',
diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
index 3a7b58e3dc9..c4ee0ad4dfb 100644
--- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
@@ -1,9 +1,9 @@
<script>
-import { GlLink } from '@gitlab/ui';
+import { GlBadge, GlLink } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
-import { formatNumber, sprintf, s__ } from '~/locale';
+import { formatNumber } from '~/locale';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
@@ -18,7 +18,7 @@ import {
I18N_FETCH_ERROR,
GROUP_FILTERED_SEARCH_NAMESPACE,
GROUP_TYPE,
- GROUP_RUNNER_COUNT_LIMIT,
+ PROJECT_TYPE,
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_STALE,
@@ -46,6 +46,7 @@ const runnersCountSmartQuery = {
export default {
name: 'GroupRunnersApp',
components: {
+ GlBadge,
GlLink,
RegistrationDropdown,
RunnerFilteredSearchBar,
@@ -131,6 +132,33 @@ export default {
};
},
},
+ allRunnersCount: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return {
+ ...this.countVariables,
+ type: null,
+ };
+ },
+ },
+ groupRunnersCount: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return {
+ ...this.countVariables,
+ type: GROUP_TYPE,
+ };
+ },
+ },
+ projectRunnersCount: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return {
+ ...this.countVariables,
+ type: PROJECT_TYPE,
+ };
+ },
+ },
},
computed: {
variables() {
@@ -139,23 +167,17 @@ export default {
groupFullPath: this.groupFullPath,
};
},
+ countVariables() {
+ // Exclude pagination variables, leave only filters variables
+ const { sort, before, last, after, first, ...countVariables } = this.variables;
+ return countVariables;
+ },
runnersLoading() {
return this.$apollo.queries.runners.loading;
},
noRunnersFound() {
return !this.runnersLoading && !this.runners.items.length;
},
- groupRunnersCount() {
- if (this.groupRunnersLimitedCount > GROUP_RUNNER_COUNT_LIMIT) {
- return `${formatNumber(GROUP_RUNNER_COUNT_LIMIT)}+`;
- }
- return formatNumber(this.groupRunnersLimitedCount);
- },
- runnerCountMessage() {
- return sprintf(s__('Runners|Runners in this group: %{groupRunnersCount}'), {
- groupRunnersCount: this.groupRunnersCount,
- });
- },
searchTokens() {
return [statusTokenConfig];
},
@@ -179,10 +201,31 @@ export default {
this.reportToSentry(error);
},
methods: {
+ tabCount({ runnerType }) {
+ let count;
+ switch (runnerType) {
+ case null:
+ count = this.allRunnersCount;
+ break;
+ case GROUP_TYPE:
+ count = this.groupRunnersCount;
+ break;
+ case PROJECT_TYPE:
+ count = this.projectRunnersCount;
+ break;
+ default:
+ return null;
+ }
+ if (typeof count === 'number') {
+ return formatNumber(count);
+ }
+ return null;
+ },
reportToSentry(error) {
captureException({ error, component: this.$options.name });
},
},
+ TABS_RUNNER_TYPES: [GROUP_TYPE, PROJECT_TYPE],
GROUP_TYPE,
};
</script>
@@ -198,9 +241,17 @@ export default {
<div class="gl-display-flex gl-align-items-center">
<runner-type-tabs
v-model="search"
+ :runner-types="$options.TABS_RUNNER_TYPES"
content-class="gl-display-none"
nav-class="gl-border-none!"
- />
+ >
+ <template #title="{ tab }">
+ {{ tab.title }}
+ <gl-badge v-if="tabCount(tab)" class="gl-ml-1" size="sm">
+ {{ tabCount(tab) }}
+ </gl-badge>
+ </template>
+ </runner-type-tabs>
<registration-dropdown
class="gl-ml-auto"
diff --git a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
index 324e0fb57cb..7cc4bc569d3 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-
module Analytics
module CycleAnalytics
module StageEventModel
@@ -16,12 +15,39 @@ module Analytics
scope :authored, ->(user) { where(author_id: user) }
scope :with_milestone_id, ->(milestone_id) { where(milestone_id: milestone_id) }
scope :end_event_is_not_happened_yet, -> { where(end_event_timestamp: nil) }
+ scope :order_by_end_event, -> (direction) do
+ # ORDER BY end_event_timestamp, merge_request_id/issue_id, start_event_timestamp
+ # start_event_timestamp must be included in the ORDER BY clause for the duration
+ # calculation to work: SELECT end_event_timestamp - start_event_timestamp
+ keyset_order(
+ :end_event_timestamp => { order_expression: arel_order(arel_table[:end_event_timestamp], direction), distinct: false },
+ issuable_id_column => { order_expression: arel_order(arel_table[issuable_id_column], direction), distinct: true },
+ :start_event_timestamp => { order_expression: arel_order(arel_table[:start_event_timestamp], direction), distinct: false }
+ )
+ end
+ scope :order_by_duration, -> (direction) do
+ # ORDER BY EXTRACT('epoch', end_event_timestamp - start_event_timestamp)
+ duration = Arel::Nodes::Subtraction.new(
+ arel_table[:end_event_timestamp],
+ arel_table[:start_event_timestamp]
+ )
+ duration_in_seconds = Arel::Nodes::Extract.new(duration, :epoch)
+
+ keyset_order(
+ :total_time => { order_expression: arel_order(duration_in_seconds, direction), distinct: false, sql_type: 'double precision' },
+ issuable_id_column => { order_expression: arel_order(arel_table[issuable_id_column], direction), distinct: true }
+ )
+ end
end
def issuable_id
attributes[self.class.issuable_id_column.to_s]
end
+ def total_time
+ read_attribute(:total_time) || (end_event_timestamp - start_event_timestamp).to_f
+ end
+
class_methods do
def upsert_data(data)
upsert_values = data.map do |row|
@@ -68,6 +94,18 @@ module Analytics
result = connection.execute(query)
result.cmd_tuples
end
+
+ def keyset_order(column_definition_options)
+ built_definitions = column_definition_options.map do |attribute_name, column_options|
+ ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(attribute_name: attribute_name, **column_options)
+ end
+
+ order(Gitlab::Pagination::Keyset::Order.build(built_definitions))
+ end
+
+ def arel_order(arel_node, direction)
+ direction.to_sym == :desc ? arel_node.desc : arel_node.asc
+ end
end
end
end
diff --git a/app/views/groups/runners/index.html.haml b/app/views/groups/runners/index.html.haml
index f904b34d29e..a67a4f28c93 100644
--- a/app/views/groups/runners/index.html.haml
+++ b/app/views/groups/runners/index.html.haml
@@ -1,6 +1,3 @@
- page_title s_('Runners|Runners')
-%h2.page-title
- = s_('Runners|Group Runners')
-
#js-group-runners{ data: group_runners_data_attributes(@group).merge( { group_runners_limited_count: @group_runners_limited_count } ) }
diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb
index 1e50c980a3a..dd8149aba94 100644
--- a/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb
+++ b/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb
@@ -37,6 +37,16 @@ module Gitlab
filter_assignees(query)
end
+ def build_sorted_query
+ direction = params[:direction] || :desc
+
+ if params[:sort] == :duration
+ build.order_by_duration(direction)
+ else
+ build.order_by_end_event(direction)
+ end
+ end
+
def filter_author(query)
return query if params[:author_username].blank?
diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/data_collector.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/data_collector.rb
index 2441b7a7497..22d8874db57 100644
--- a/lib/gitlab/analytics/cycle_analytics/aggregated/data_collector.rb
+++ b/lib/gitlab/analytics/cycle_analytics/aggregated/data_collector.rb
@@ -32,7 +32,7 @@ module Gitlab
def records_fetcher
strong_memoize(:records_fetcher) do
- RecordsFetcher.new(stage: stage, query: query, params: params)
+ RecordsFetcher.new(stage: stage, query: query_builder.build_sorted_query, params: params)
end
end
@@ -41,7 +41,11 @@ module Gitlab
attr_reader :stage, :params
def query
- BaseQueryBuilder.new(stage: stage, params: params).build
+ query_builder.build
+ end
+
+ def query_builder
+ @query_builder = BaseQueryBuilder.new(stage: stage, params: params)
end
def limit_count
diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb
index 1742d396c10..55e421173d7 100644
--- a/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb
+++ b/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb
@@ -36,7 +36,13 @@ module Gitlab
def serialized_records
strong_memoize(:serialized_records) do
- records = ordered_and_limited_query.select(stage_event_model.arel_table[Arel.star], duration_in_seconds.as('total_time'))
+ # When RecordsFetcher is used with query sourced from
+ # InOperatorOptimization::QueryBuilder only columns
+ # used in ORDER BY statement would be selected by Arel.start operation
+ selections = [stage_event_model.arel_table[Arel.star]]
+ selections << duration_in_seconds.as('total_time') if params[:sort] != :duration # duration sorting already exposes this data
+
+ records = limited_query.select(*selections)
yield records if block_given?
issuables_and_records = load_issuables(records)
@@ -56,27 +62,12 @@ module Gitlab
end
end
- # rubocop: disable CodeReuse/ActiveRecord
- def ordered_and_limited_query
- sorting_options = {
- end_event: {
- asc: -> { query.order(end_event_timestamp: :asc) },
- desc: -> { query.order(end_event_timestamp: :desc) }
- },
- duration: {
- asc: -> { query.order(duration.asc) },
- desc: -> { query.order(duration.desc) }
- }
- }
-
- sort_lambda = sorting_options.dig(sort, direction) || sorting_options.dig(:end_event, :desc)
-
- sort_lambda.call
+ def limited_query
+ query
.page(page)
.per(per_page)
.without_count
end
- # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/lib/gitlab/analytics/cycle_analytics/data_collector.rb b/lib/gitlab/analytics/cycle_analytics/data_collector.rb
index a20481dd39e..ae675b6ad27 100644
--- a/lib/gitlab/analytics/cycle_analytics/data_collector.rb
+++ b/lib/gitlab/analytics/cycle_analytics/data_collector.rb
@@ -62,7 +62,11 @@ module Gitlab
attr_reader :stage, :params
def query
- BaseQueryBuilder.new(stage: stage, params: params).build
+ query_builder.build
+ end
+
+ def query_builder
+ @query_builder ||= BaseQueryBuilder.new(stage: stage, params: params)
end
# Limiting the maximum number of records so the COUNT(*) query stays efficient for large groups.
diff --git a/lib/tasks/gitlab/banzai.rake b/lib/tasks/gitlab/banzai.rake
new file mode 100644
index 00000000000..b1c7e4ea519
--- /dev/null
+++ b/lib/tasks/gitlab/banzai.rake
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+namespace :gitlab do
+ namespace :banzai do
+ desc 'GitLab | Banzai | Render markdown using our FullPipeline (input will be requested)'
+ task render: :environment do |_t|
+ markdown = []
+
+ puts "\nEnter markdown below, Ctrl-D to end (if you need blank lines, paste in the full text):"
+ while buf = Readline.readline('', true)
+ markdown << buf
+ end
+
+ puts "Rendering using Gitlab's FullPipeline...\n\n"
+
+ html = MarkupHelper.markdown(markdown.join("\n"), { pipeline: :full, project: nil })
+ puts html.gsub('&#x000A;', "\n")
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3babccc6701..35f56271abd 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -30834,9 +30834,6 @@ msgstr ""
msgid "Runners|Group"
msgstr ""
-msgid "Runners|Group Runners"
-msgstr ""
-
msgid "Runners|IP Address"
msgstr ""
@@ -30969,9 +30966,6 @@ msgstr ""
msgid "Runners|Runners"
msgstr ""
-msgid "Runners|Runners in this group: %{groupRunnersCount}"
-msgstr ""
-
msgid "Runners|Runs untagged jobs"
msgstr ""
diff --git a/qa/qa/tools/delete_test_resources.rb b/qa/qa/tools/delete_test_resources.rb
index 917cb2fa992..31499559481 100644
--- a/qa/qa/tools/delete_test_resources.rb
+++ b/qa/qa/tools/delete_test_resources.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-# This script reads from test_resources.txt file to collect data about resources to delete
-# Deletes all deletable resources that E2E tests created
-# Resource type: Sandbox, User, Fork and RSpec::Mocks::Double are not included
+# This script reads from test-resources JSON file to collect data about resources to delete
+# Filter out resources that cannot be deleted
+# Then deletes all deletable resources that E2E tests created
#
# Required environment variables: GITLAB_QA_ACCESS_TOKEN and GITLAB_ADDRESS
# When in CI also requires: QA_TEST_RESOURCES_FILE_PATTERN
@@ -13,7 +13,15 @@ module QA
class DeleteTestResources
include Support::API
- def initialize(file_pattern = nil)
+ IGNORED_RESOURCES = [
+ 'QA::Resource::PersonalAccessToken',
+ 'QA::Resource::CiVariable',
+ 'QA::Resource::Repository::Commit',
+ 'QA::EE::Resource::GroupIteration',
+ 'QA::EE::Resource::Settings::Elasticsearch'
+ ].freeze
+
+ def initialize(file_pattern = Runtime::Env.test_resources_created_filepath)
raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS']
raise ArgumentError, "Please provide GITLAB_QA_ACCESS_TOKEN" unless ENV['GITLAB_QA_ACCESS_TOKEN']
@@ -22,45 +30,58 @@ module QA
end
def run
- puts 'Deleting test created resources...'
-
- if Runtime::Env.running_in_ci?
- raise ArgumentError, 'Please provide QA_TEST_RESOURCES_FILE_PATTERN' unless ENV['QA_TEST_RESOURCES_FILE_PATTERN']
-
- Dir.glob(@file_pattern).each do |file|
- delete_resources(load_file(file))
- end
- else
- file = Runtime::Env.test_resources_created_filepath
- raise ArgumentError, "'#{file}' either does not exist or empty." if !File.exist?(file) || File.zero?(file)
-
- delete_resources(load_file(file))
+ failures = files.flat_map do |file|
+ resources = read_file(file)
+ filtered_resources = filter_resources(resources)
+ delete_resources(filtered_resources)
end
- puts "\nDone"
+ return puts "\nDone" if failures.empty?
+
+ puts "\nFailed to delete #{failures.size} resources:\n"
+ puts failures
end
private
- def load_file(json)
- JSON.parse(File.read(json))
+ def files
+ puts "Gathering JSON files...\n"
+ files = Dir.glob(@file_pattern)
+ abort("There is no file with this pattern #{@file_pattern}") if files.empty?
+
+ files.reject { |file| File.zero?(file) }
+
+ files
end
- def delete_resources(resources)
- failures = []
+ def read_file(file)
+ JSON.parse(File.read(file))
+ end
- resources.each_key do |type|
- next if resources[type].empty?
+ def filter_resources(resources)
+ puts "Filtering resources - Only keep deletable resources...\n"
- resources[type].each do |resource|
- next if resource_not_found?(resource['api_path'])
+ transformed_values = resources.transform_values! do |v|
+ v.reject do |attributes|
+ attributes['info'] == "with full_path 'gitlab-qa-sandbox-group'" ||
+ attributes['http_method'] == 'get' && !attributes['info'].include?("with username 'qa-") ||
+ attributes['api_path'] == 'Cannot find resource API path'
+ end
+ end
+
+ transformed_values.reject! { |k, v| v.empty? || IGNORED_RESOURCES.include?(k) }
+ end
- msg = resource['info'] ? "#{type} - #{resource['info']}" : "#{type} at #{resource['api_path']}"
+ def delete_resources(resources)
+ resources.each_with_object([]) do |(key, value), failures|
+ value.each do |resource|
+ next if resource_not_found?(resource['api_path'])
+ msg = resource['info'] ? "#{key} - #{resource['info']}" : "#{key} at #{resource['api_path']}"
puts "\nDeleting #{msg}..."
delete_response = delete(Runtime::API::Request.new(@api_client, resource['api_path']).url)
- if delete_response.code == 202
+ if delete_response.code == 202 || delete_response.code == 204
print "\e[32m.\e[0m"
else
print "\e[31mF\e[0m"
@@ -68,17 +89,11 @@ module QA
end
end
end
-
- unless failures.empty?
- puts "\nFailed to delete #{failures.length} resources:\n"
- puts failures
- end
end
def resource_not_found?(api_path)
- get_response = get Runtime::API::Request.new(@api_client, api_path).url
-
- get_response.code.eql? 404
+ # if api path contains param "?hard_delete=<boolean>", remove it
+ get(Runtime::API::Request.new(@api_client, api_path.split('?').first).url).code.eql? 404
end
end
end
diff --git a/spec/frontend/runner/components/runner_type_tabs_spec.js b/spec/frontend/runner/components/runner_type_tabs_spec.js
index 4871d9c470a..9da5d842d8f 100644
--- a/spec/frontend/runner/components/runner_type_tabs_spec.js
+++ b/spec/frontend/runner/components/runner_type_tabs_spec.js
@@ -1,7 +1,7 @@
import { GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
-import { INSTANCE_TYPE, GROUP_TYPE } from '~/runner/constants';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
const mockSearch = { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' };
@@ -13,6 +13,7 @@ describe('RunnerTypeTabs', () => {
findTabs()
.filter((tab) => tab.attributes('active') === 'true')
.at(0);
+ const getTabsTitles = () => findTabs().wrappers.map((tab) => tab.text());
const createComponent = ({ props, ...options } = {}) => {
wrapper = shallowMount(RunnerTypeTabs, {
@@ -35,13 +36,18 @@ describe('RunnerTypeTabs', () => {
wrapper.destroy();
});
- it('Renders options to filter runners', () => {
- expect(findTabs().wrappers.map((tab) => tab.text())).toEqual([
- 'All',
- 'Instance',
- 'Group',
- 'Project',
- ]);
+ it('Renders all options to filter runners by default', () => {
+ expect(getTabsTitles()).toEqual(['All', 'Instance', 'Group', 'Project']);
+ });
+
+ it('Renders fewer options to filter runners', () => {
+ createComponent({
+ props: {
+ runnerTypes: [GROUP_TYPE, PROJECT_TYPE],
+ },
+ });
+
+ expect(getTabsTitles()).toEqual(['All', 'Group', 'Project']);
});
it('"All" is selected by default', () => {
diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js
index 7872cfd6cf4..f1802ac78a1 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -1,16 +1,19 @@
import Vue, { nextTick } from 'vue';
import { GlLink } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
-
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import {
+ extendedWrapper,
+ shallowMountExtended,
+ mountExtended,
+} from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
+import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
@@ -23,6 +26,7 @@ import {
DEFAULT_SORT,
INSTANCE_TYPE,
GROUP_TYPE,
+ PROJECT_TYPE,
PARAM_KEY_STATUS,
STATUS_ACTIVE,
RUNNER_PAGE_SIZE,
@@ -54,6 +58,7 @@ describe('GroupRunnersApp', () => {
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
+ const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
const findRunnerPaginationPrev = () =>
@@ -62,7 +67,12 @@ describe('GroupRunnersApp', () => {
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
- const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
+ const mockCountQueryResult = (count) =>
+ Promise.resolve({
+ data: { group: { id: groupRunnersCountData.data.group.id, runners: { count } } },
+ });
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
const handlers = [
[getGroupRunnersQuery, mockGroupRunnersQuery],
[getGroupRunnersCountQuery, mockGroupRunnersCountQuery],
@@ -90,7 +100,7 @@ describe('GroupRunnersApp', () => {
});
it('shows total runner counts', async () => {
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
await waitForPromises();
@@ -101,6 +111,44 @@ describe('GroupRunnersApp', () => {
expect(stats).toMatch('Stale runners 2');
});
+ it('shows the runner tabs with a runner count for each type', async () => {
+ mockGroupRunnersCountQuery.mockImplementation(({ type }) => {
+ switch (type) {
+ case GROUP_TYPE:
+ return mockCountQueryResult(2);
+ case PROJECT_TYPE:
+ return mockCountQueryResult(1);
+ default:
+ return mockCountQueryResult(4);
+ }
+ });
+
+ createComponent({ mountFn: mountExtended });
+ await waitForPromises();
+
+ expect(findRunnerTypeTabs().text()).toMatchInterpolatedText('All 4 Group 2 Project 1');
+ });
+
+ it('shows the runner tabs with a formatted runner count', async () => {
+ mockGroupRunnersCountQuery.mockImplementation(({ type }) => {
+ switch (type) {
+ case GROUP_TYPE:
+ return mockCountQueryResult(2000);
+ case PROJECT_TYPE:
+ return mockCountQueryResult(1000);
+ default:
+ return mockCountQueryResult(3000);
+ }
+ });
+
+ createComponent({ mountFn: mountExtended });
+ await waitForPromises();
+
+ expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
+ 'All 3,000 Group 2,000 Project 1,000',
+ );
+ });
+
it('shows the runner setup instructions', () => {
expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken);
expect(findRegistrationDropdown().props('type')).toBe(GROUP_TYPE);
@@ -115,7 +163,7 @@ describe('GroupRunnersApp', () => {
const { webUrl, node } = groupRunnersData.data.group.runners.edges[0];
const { id, shortSha } = node;
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
await waitForPromises();
@@ -135,7 +183,7 @@ describe('GroupRunnersApp', () => {
});
it('sets tokens in the filtered search', () => {
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
const tokens = findFilteredSearch().props('tokens');
@@ -250,7 +298,7 @@ describe('GroupRunnersApp', () => {
beforeEach(() => {
mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersDataPaginated);
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
});
it('more pages can be selected', () => {
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb
index 55ba6e56237..8eb75feaa8d 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb
@@ -8,16 +8,17 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher do
let_it_be(:issue_2) { create(:issue, project: project) }
let_it_be(:issue_3) { create(:issue, project: project) }
- let_it_be(:stage_event_1) { create(:cycle_analytics_issue_stage_event, issue_id: issue_1.id, start_event_timestamp: 2.years.ago, end_event_timestamp: 1.year.ago) } # duration: 1 year
- let_it_be(:stage_event_2) { create(:cycle_analytics_issue_stage_event, issue_id: issue_2.id, start_event_timestamp: 5.years.ago, end_event_timestamp: 2.years.ago) } # duration: 3 years
- let_it_be(:stage_event_3) { create(:cycle_analytics_issue_stage_event, issue_id: issue_3.id, start_event_timestamp: 6.years.ago, end_event_timestamp: 3.months.ago) } # duration: 5+ years
-
let_it_be(:stage) { create(:cycle_analytics_project_stage, start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production, project: project) }
- let(:params) { {} }
+ let_it_be(:stage_event_1) { create(:cycle_analytics_issue_stage_event, stage_event_hash_id: stage.stage_event_hash_id, project_id: project.id, issue_id: issue_1.id, start_event_timestamp: 2.years.ago, end_event_timestamp: 1.year.ago) } # duration: 1 year
+ let_it_be(:stage_event_2) { create(:cycle_analytics_issue_stage_event, stage_event_hash_id: stage.stage_event_hash_id, project_id: project.id, issue_id: issue_2.id, start_event_timestamp: 5.years.ago, end_event_timestamp: 2.years.ago) } # duration: 3 years
+ let_it_be(:stage_event_3) { create(:cycle_analytics_issue_stage_event, stage_event_hash_id: stage.stage_event_hash_id, project_id: project.id, issue_id: issue_3.id, start_event_timestamp: 6.years.ago, end_event_timestamp: 3.months.ago) } # duration: 5+ years
+
+ let(:params) { { from: 10.years.ago, to: Date.today } }
subject(:records_fetcher) do
- described_class.new(stage: stage, query: Analytics::CycleAnalytics::IssueStageEvent.all, params: params)
+ query_builder = Gitlab::Analytics::CycleAnalytics::Aggregated::BaseQueryBuilder.new(stage: stage, params: params)
+ described_class.new(stage: stage, query: query_builder.build_sorted_query, params: params)
end
shared_examples 'match returned records' do
@@ -123,6 +124,8 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher do
it 'skips non-existing issue records' do
create(:cycle_analytics_issue_stage_event, {
issue_id: 0, # non-existing id
+ stage_event_hash_id: stage.stage_event_hash_id,
+ project_id: project.id,
start_event_timestamp: 5.months.ago,
end_event_timestamp: 3.months.ago
})
diff --git a/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb b/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb
index d823e7ac221..8ff30021d6e 100644
--- a/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb
+++ b/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb
@@ -178,4 +178,22 @@ RSpec.shared_examples 'StageEventModel' do
end
end
end
+
+ describe '#total_time' do
+ it 'calcualtes total time from the start_event_timestamp and end_event_timestamp columns' do
+ model = described_class.new(start_event_timestamp: Time.new(2022, 1, 1, 12, 5, 0), end_event_timestamp: Time.new(2022, 1, 1, 12, 6, 30))
+
+ expect(model.total_time).to eq(90)
+ end
+
+ context 'when total time is calculated in SQL as an extra column' do
+ it 'returns the SQL calculated time' do
+ create(stage_event_factory) # rubocop:disable Rails/SaveBang
+
+ model = described_class.select('*, 5 AS total_time').first
+
+ expect(model.total_time).to eq(5)
+ end
+ end
+ end
end
diff --git a/spec/tooling/danger/project_helper_spec.rb b/spec/tooling/danger/project_helper_spec.rb
index 52aa90beb2b..1b416286f8e 100644
--- a/spec/tooling/danger/project_helper_spec.rb
+++ b/spec/tooling/danger/project_helper_spec.rb
@@ -209,6 +209,7 @@ RSpec.describe Tooling::Danger::ProjectHelper do
'lib/api/entities/project_integration.rb' | [:integrations_be, :backend]
'lib/gitlab/hook_data/note_builder.rb' | [:integrations_be, :backend]
'lib/gitlab/data_builder/note.rb' | [:integrations_be, :backend]
+ 'lib/gitlab/web_hooks/recursion_detection.rb' | [:integrations_be, :backend]
'ee/lib/ee/gitlab/integrations/sti_type.rb' | [:integrations_be, :backend]
'ee/lib/ee/api/helpers/integrations_helpers.rb' | [:integrations_be, :backend]
'ee/app/serializers/integrations/jira_serializers/issue_entity.rb' | [:integrations_be, :backend]
diff --git a/tooling/danger/project_helper.rb b/tooling/danger/project_helper.rb
index f49b8bf7f2a..cc80411e750 100644
--- a/tooling/danger/project_helper.rb
+++ b/tooling/danger/project_helper.rb
@@ -56,7 +56,7 @@ module Tooling
((ee|jh)/)?app/controllers/(.+/)?oauth/jira/.+ |
((ee|jh)/)?app/services/(.+/)?jira.+ |
((ee|jh)/)?app/workers/(.+/)?(propagate_integration.+|irker_worker\.rb) |
- ((ee|jh)/)?lib/(.+/)?(atlassian|data_builder|hook_data)/.+ |
+ ((ee|jh)/)?lib/(.+/)?(atlassian|data_builder|hook_data|web_hooks)/.+ |
((ee|jh)/)?lib/(.+/)?.*integration.+ |
((ee|jh)/)?lib/(.+/)?api/v3/github\.rb |
((ee|jh)/)?lib/(.+/)?api/github/entities\.rb