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>2019-09-18 18:06:03 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2019-09-18 18:06:03 +0300
commit4584eb0e07d372d6014de16ab359965475184c99 (patch)
tree586f35b77fac7ddfb8a05aa57b4fb6aa3d98212e
parentbdbded586beb38e2ee4642c6a1e78ccbebc094dc (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/import_projects/components/import_projects_table.vue45
-rw-r--r--app/assets/javascripts/import_projects/index.js2
-rw-r--r--app/assets/javascripts/import_projects/store/actions.js22
-rw-r--r--app/assets/javascripts/import_projects/store/getters.js5
-rw-r--r--app/assets/javascripts/import_projects/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/import_projects/store/mutations.js4
-rw-r--r--app/assets/javascripts/import_projects/store/state.js1
-rw-r--r--app/controllers/import/github_controller.rb21
-rw-r--r--changelogs/unreleased/georgekoltsov-add-github-importer-filtering.yml5
-rw-r--r--doc/user/project/import/gitea.md10
-rw-r--r--doc/user/project/import/github.md5
-rw-r--r--doc/user/project/import/img/import_projects_from_gitea_importer_v12_3.pngbin0 -> 50650 bytes
-rw-r--r--doc/user/project/import/img/import_projects_from_github_importer.pngbin17953 -> 0 bytes
-rw-r--r--doc/user/project/import/img/import_projects_from_github_importer_v12_3.pngbin0 -> 53497 bytes
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events.rb6
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb15
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/issue_created.rb4
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit.rb10
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb13
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created.rb4
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb10
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished.rb10
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started.rb10
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged.rb10
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb16
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb33
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb17
-rw-r--r--locale/gitlab.pot4
-rw-r--r--spec/frontend/import_projects/components/import_projects_table_spec.js8
-rw-r--r--spec/frontend/import_projects/store/actions_spec.js64
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start_spec.rb22
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created_spec.rb7
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit_spec.rb7
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end_spec.rb7
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created_spec.rb7
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production_spec.rb7
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished_spec.rb7
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started_spec.rb7
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged_spec.rb7
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start_spec.rb24
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb7
-rw-r--r--spec/support/controllers/githubish_import_controller_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/cycle_analytics_event_shared_examples.rb19
43 files changed, 485 insertions, 31 deletions
diff --git a/app/assets/javascripts/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_projects/components/import_projects_table.vue
index 00eb0afb3bf..e5ac3cbafe5 100644
--- a/app/assets/javascripts/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_projects/components/import_projects_table.vue
@@ -1,4 +1,5 @@
<script>
+import _ from 'underscore';
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
@@ -7,6 +8,8 @@ import ImportedProjectTableRow from './imported_project_table_row.vue';
import ProviderRepoTableRow from './provider_repo_table_row.vue';
import eventHub from '../event_hub';
+const reposFetchThrottleDelay = 1000;
+
export default {
name: 'ImportProjectsTable',
components: {
@@ -23,11 +26,11 @@ export default {
},
computed: {
- ...mapState(['importedProjects', 'providerRepos', 'isLoadingRepos']),
+ ...mapState(['importedProjects', 'providerRepos', 'isLoadingRepos', 'filter']),
...mapGetters(['isImportingAnyRepo', 'hasProviderRepos', 'hasImportedProjects']),
emptyStateText() {
- return sprintf(__('No %{providerTitle} repositories available to import'), {
+ return sprintf(__('No %{providerTitle} repositories found'), {
providerTitle: this.providerTitle,
});
},
@@ -47,21 +50,38 @@ export default {
},
methods: {
- ...mapActions(['fetchRepos', 'fetchJobs', 'stopJobsPolling', 'clearJobsEtagPoll']),
+ ...mapActions([
+ 'fetchRepos',
+ 'fetchReposFiltered',
+ 'fetchJobs',
+ 'stopJobsPolling',
+ 'clearJobsEtagPoll',
+ 'setFilter',
+ ]),
importAll() {
eventHub.$emit('importAll');
},
+
+ handleFilterInput({ target }) {
+ this.setFilter(target.value);
+ },
+
+ throttledFetchRepos: _.throttle(function fetch() {
+ eventHub.$off('importAll');
+ this.fetchRepos();
+ }, reposFetchThrottleDelay),
},
};
</script>
<template>
<div>
+ <p class="light text-nowrap mt-2">
+ {{ s__('ImportProjects|Select the projects you want to import') }}
+ </p>
+
<div class="d-flex justify-content-between align-items-end flex-wrap mb-3">
- <p class="light text-nowrap mt-2 my-sm-0">
- {{ s__('ImportProjects|Select the projects you want to import') }}
- </p>
<loading-button
container-class="btn btn-success js-import-all"
:loading="isImportingAnyRepo"
@@ -70,6 +90,19 @@ export default {
type="button"
@click="importAll"
/>
+ <form novalidate @submit.prevent>
+ <input
+ :value="filter"
+ data-qa-selector="githubish_import_filter_field"
+ class="form-control"
+ name="filter"
+ :placeholder="__('Filter your projects by name')"
+ autofocus
+ size="40"
+ @input="handleFilterInput($event)"
+ @keyup.enter="throttledFetchRepos"
+ />
+ </form>
</div>
<gl-loading-icon
v-if="isLoadingRepos"
diff --git a/app/assets/javascripts/import_projects/index.js b/app/assets/javascripts/import_projects/index.js
index 2d99d716609..b069dcb7766 100644
--- a/app/assets/javascripts/import_projects/index.js
+++ b/app/assets/javascripts/import_projects/index.js
@@ -38,7 +38,7 @@ export default function mountImportProjectsTable(mountElement) {
},
methods: {
- ...mapActions(['setInitialData']),
+ ...mapActions(['setInitialData', 'setFilter']),
},
render(createElement) {
diff --git a/app/assets/javascripts/import_projects/store/actions.js b/app/assets/javascripts/import_projects/store/actions.js
index c44500937cc..0fb9a4cdfd4 100644
--- a/app/assets/javascripts/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_projects/store/actions.js
@@ -5,6 +5,7 @@ import Poll from '~/lib/utils/poll';
import createFlash from '~/flash';
import { s__, sprintf } from '~/locale';
import axios from '~/lib/utils/axios_utils';
+import { jobsPathWithFilter, reposPathWithFilter } from './getters';
let eTagPoll;
@@ -19,16 +20,20 @@ export const restartJobsPolling = () => {
};
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
+export const setFilter = ({ commit }, filter) => commit(types.SET_FILTER, filter);
export const requestRepos = ({ commit }, repos) => commit(types.REQUEST_REPOS, repos);
export const receiveReposSuccess = ({ commit }, repos) =>
commit(types.RECEIVE_REPOS_SUCCESS, repos);
export const receiveReposError = ({ commit }) => commit(types.RECEIVE_REPOS_ERROR);
export const fetchRepos = ({ state, dispatch }) => {
+ dispatch('stopJobsPolling');
dispatch('requestRepos');
+ const { provider } = state;
+
return axios
- .get(state.reposPath)
+ .get(reposPathWithFilter(state))
.then(({ data }) =>
dispatch('receiveReposSuccess', convertObjectPropsToCamelCase(data, { deep: true })),
)
@@ -36,7 +41,7 @@ export const fetchRepos = ({ state, dispatch }) => {
.catch(() => {
createFlash(
sprintf(s__('ImportProjects|Requesting your %{provider} repositories failed'), {
- provider: state.provider,
+ provider,
}),
);
@@ -77,16 +82,23 @@ export const fetchImport = ({ state, dispatch }, { newName, targetNamespace, rep
export const receiveJobsSuccess = ({ commit }, updatedProjects) =>
commit(types.RECEIVE_JOBS_SUCCESS, updatedProjects);
export const fetchJobs = ({ state, dispatch }) => {
- if (eTagPoll) return;
+ const { filter } = state;
+
+ if (eTagPoll) {
+ stopJobsPolling();
+ clearJobsEtagPoll();
+ }
eTagPoll = new Poll({
resource: {
- fetchJobs: () => axios.get(state.jobsPath),
+ fetchJobs: () => axios.get(jobsPathWithFilter(state)),
},
method: 'fetchJobs',
successCallback: ({ data }) =>
dispatch('receiveJobsSuccess', convertObjectPropsToCamelCase(data, { deep: true })),
- errorCallback: () => createFlash(s__('ImportProjects|Updating the imported projects failed')),
+ errorCallback: () =>
+ createFlash(s__('ImportProjects|Update of imported projects with realtime changes failed')),
+ data: { filter },
});
if (!Visibility.hidden()) {
diff --git a/app/assets/javascripts/import_projects/store/getters.js b/app/assets/javascripts/import_projects/store/getters.js
index 727b80765bd..b107c293181 100644
--- a/app/assets/javascripts/import_projects/store/getters.js
+++ b/app/assets/javascripts/import_projects/store/getters.js
@@ -20,3 +20,8 @@ export const isImportingAnyRepo = state => state.reposBeingImported.length > 0;
export const hasProviderRepos = state => state.providerRepos.length > 0;
export const hasImportedProjects = state => state.importedProjects.length > 0;
+
+export const reposPathWithFilter = ({ reposPath, filter = '' }) =>
+ filter ? `${reposPath}?filter=${filter}` : reposPath;
+export const jobsPathWithFilter = ({ jobsPath, filter = '' }) =>
+ filter ? `${jobsPath}?filter=${filter}` : jobsPath;
diff --git a/app/assets/javascripts/import_projects/store/mutation_types.js b/app/assets/javascripts/import_projects/store/mutation_types.js
index 6ba3fd6f29e..16574f4450f 100644
--- a/app/assets/javascripts/import_projects/store/mutation_types.js
+++ b/app/assets/javascripts/import_projects/store/mutation_types.js
@@ -9,3 +9,5 @@ export const RECEIVE_IMPORT_SUCCESS = 'RECEIVE_IMPORT_SUCCESS';
export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR';
export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
+
+export const SET_FILTER = 'SET_FILTER';
diff --git a/app/assets/javascripts/import_projects/store/mutations.js b/app/assets/javascripts/import_projects/store/mutations.js
index b88de0268e7..6c56cfa8298 100644
--- a/app/assets/javascripts/import_projects/store/mutations.js
+++ b/app/assets/javascripts/import_projects/store/mutations.js
@@ -6,6 +6,10 @@ export default {
Object.assign(state, data);
},
+ [types.SET_FILTER](state, filter) {
+ state.filter = filter;
+ },
+
[types.REQUEST_REPOS](state) {
state.isLoadingRepos = true;
},
diff --git a/app/assets/javascripts/import_projects/store/state.js b/app/assets/javascripts/import_projects/store/state.js
index 637fef6e53c..829f3aa4fbb 100644
--- a/app/assets/javascripts/import_projects/store/state.js
+++ b/app/assets/javascripts/import_projects/store/state.js
@@ -12,4 +12,5 @@ export default () => ({
isLoadingRepos: false,
canSelectNamespace: false,
ciCdOnly: false,
+ filter: '',
});
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 72f830fc9a1..c418b11ab13 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -2,6 +2,7 @@
class Import::GithubController < Import::BaseController
include ImportHelper
+ include ActionView::Helpers::SanitizeHelper
before_action :verify_import_enabled
before_action :provider_auth, only: [:status, :realtime_changes, :create]
@@ -55,7 +56,7 @@ class Import::GithubController < Import::BaseController
def realtime_changes
Gitlab::PollingInterval.set_header(response, interval: 3_000)
- render json: find_jobs(provider)
+ render json: already_added_projects.to_json(only: [:id], methods: [:import_status])
end
private
@@ -82,7 +83,7 @@ class Import::GithubController < Import::BaseController
end
def already_added_projects
- @already_added_projects ||= find_already_added_projects(provider)
+ @already_added_projects ||= filtered(find_already_added_projects(provider))
end
def already_added_project_names
@@ -104,7 +105,7 @@ class Import::GithubController < Import::BaseController
end
def client_repos
- @client_repos ||= client.repos
+ @client_repos ||= filtered(client.repos)
end
def verify_import_enabled
@@ -185,6 +186,20 @@ class Import::GithubController < Import::BaseController
def extra_import_params
{}
end
+
+ def sanitized_filter_param
+ @filter ||= sanitize(params[:filter])
+ end
+
+ def filter_attribute
+ :name
+ end
+
+ def filtered(collection)
+ return collection unless sanitized_filter_param
+
+ collection.select { |item| item[filter_attribute].include?(sanitized_filter_param) }
+ end
end
Import::GithubController.prepend_if_ee('EE::Import::GithubController')
diff --git a/changelogs/unreleased/georgekoltsov-add-github-importer-filtering.yml b/changelogs/unreleased/georgekoltsov-add-github-importer-filtering.yml
new file mode 100644
index 00000000000..9c7a8caea66
--- /dev/null
+++ b/changelogs/unreleased/georgekoltsov-add-github-importer-filtering.yml
@@ -0,0 +1,5 @@
+---
+title: Add GitHub & Gitea importers project filtering
+merge_request: 16823
+author:
+type: added
diff --git a/doc/user/project/import/gitea.md b/doc/user/project/import/gitea.md
index f5746a0fb31..0b9034c821b 100644
--- a/doc/user/project/import/gitea.md
+++ b/doc/user/project/import/gitea.md
@@ -66,10 +66,14 @@ From there, you can see the import statuses of your Gitea repositories.
- whereas those that are not yet imported will have an **Import** button on the
right side of the table.
-If you want, you can import all your Gitea projects in one go by hitting
-**Import all projects** in the upper left corner.
+You also can:
-![Gitea importer page](img/import_projects_from_github_importer.png)
+- Import all your Gitea projects in one go by hitting **Import all projects** in
+ the upper left corner
+- Filter projects by name. If filter is applied, hitting **Import all projects**
+ will only import matched projects
+
+![Gitea importer page](img/import_projects_from_gitea_importer_v12_3.png)
---
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
index dad53a600dc..0fd724f63ac 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -115,11 +115,14 @@ your GitHub repositories are listed.
1. By default, the proposed repository namespaces match the names as they exist in GitHub, but based on your permissions,
you can choose to edit these names before you proceed to import any of them.
-1. Select the **Import** button next to any number of repositories, or select **Import all repositories**.
+1. Select the **Import** button next to any number of repositories, or select **Import all repositories**. Additionally,
+ you can filter projects by name. If filter is applied, **Import all repositories** only imports matched repositories.
1. The **Status** column shows the import status of each repository. You can choose to leave the page open and it will
update in realtime or you can return to it later.
1. Once a repository has been imported, click its GitLab path to open its GitLab URL.
+![Github importer page](img/import_projects_from_github_importer_v12_3.png)
+
## Mirroring and pipeline status sharing
Depending your GitLab tier, [project mirroring](../../../workflow/repository_mirroring.md) can be set up to keep
diff --git a/doc/user/project/import/img/import_projects_from_gitea_importer_v12_3.png b/doc/user/project/import/img/import_projects_from_gitea_importer_v12_3.png
new file mode 100644
index 00000000000..d8ae1a54851
--- /dev/null
+++ b/doc/user/project/import/img/import_projects_from_gitea_importer_v12_3.png
Binary files differ
diff --git a/doc/user/project/import/img/import_projects_from_github_importer.png b/doc/user/project/import/img/import_projects_from_github_importer.png
deleted file mode 100644
index d8effaf6075..00000000000
--- a/doc/user/project/import/img/import_projects_from_github_importer.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/import/img/import_projects_from_github_importer_v12_3.png b/doc/user/project/import/img/import_projects_from_github_importer_v12_3.png
new file mode 100644
index 00000000000..6a53d9e6d1d
--- /dev/null
+++ b/doc/user/project/import/img/import_projects_from_github_importer_v12_3.png
Binary files differ
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events.rb b/lib/gitlab/analytics/cycle_analytics/stage_events.rb
index d21f344f483..58572446de6 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events.rb
@@ -18,7 +18,8 @@ module Gitlab
StageEvents::MergeRequestMerged => 104,
StageEvents::CodeStageStart => 1_000,
StageEvents::IssueStageEnd => 1_001,
- StageEvents::PlanStageStart => 1_002
+ StageEvents::PlanStageStart => 1_002,
+ StageEvents::ProductionStageEnd => 1_003
}.freeze
EVENTS = ENUM_MAPPING.keys.freeze
@@ -32,7 +33,8 @@ module Gitlab
StageEvents::MergeRequestCreated
],
StageEvents::IssueCreated => [
- StageEvents::IssueStageEnd
+ StageEvents::IssueStageEnd,
+ StageEvents::ProductionStageEnd
],
StageEvents::MergeRequestCreated => [
StageEvents::MergeRequestMerged
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb
index ff9c8a79225..6af1b90bccc 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb
@@ -16,6 +16,21 @@ module Gitlab
def object_type
MergeRequest
end
+
+ def timestamp_projection
+ issue_metrics_table[:first_mentioned_in_commit_at]
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def apply_query_customization(query)
+ issue_metrics_join = mr_closing_issues_table
+ .join(issue_metrics_table)
+ .on(mr_closing_issues_table[:issue_id].eq(issue_metrics_table[:issue_id]))
+ .join_sources
+
+ query.joins(:merge_requests_closing_issues).joins(issue_metrics_join)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created.rb
index a601c9797f8..8c9a80740a9 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created.rb
@@ -16,6 +16,10 @@ module Gitlab
def object_type
Issue
end
+
+ def timestamp_projection
+ issue_table[:created_at]
+ end
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit.rb
index 7424043ef7b..fe7f2d85f8b 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit.rb
@@ -16,6 +16,16 @@ module Gitlab
def object_type
Issue
end
+
+ def timestamp_projection
+ issue_metrics_table[:first_mentioned_in_commit_at]
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def apply_query_customization(query)
+ query.joins(:metrics)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb
index ceb229c552f..77e4092b9ab 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb
@@ -16,6 +16,19 @@ module Gitlab
def object_type
Issue
end
+
+ def timestamp_projection
+ Arel::Nodes::NamedFunction.new('COALESCE', [
+ issue_metrics_table[:first_associated_with_milestone_at],
+ issue_metrics_table[:first_added_to_board_at]
+ ])
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def apply_query_customization(query)
+ query.joins(:metrics).where(issue_metrics_table[:first_added_to_board_at].not_eq(nil).or(issue_metrics_table[:first_associated_with_milestone_at].not_eq(nil)))
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created.rb
index 8be00831b4f..7059c425b8f 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created.rb
@@ -16,6 +16,10 @@ module Gitlab
def object_type
MergeRequest
end
+
+ def timestamp_projection
+ mr_table[:created_at]
+ end
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb
index 6d7a2c023ff..3d7482eaaf0 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb
@@ -16,6 +16,16 @@ module Gitlab
def object_type
MergeRequest
end
+
+ def timestamp_projection
+ mr_metrics_table[:first_deployed_to_production_at]
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def apply_query_customization(query)
+ query.joins(:metrics).where(timestamp_projection.gteq(mr_table[:created_at]))
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished.rb
index 12d82fe2c62..36bb4d6fc8d 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished.rb
@@ -16,6 +16,16 @@ module Gitlab
def object_type
MergeRequest
end
+
+ def timestamp_projection
+ mr_metrics_table[:latest_build_finished_at]
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def apply_query_customization(query)
+ query.joins(:metrics)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started.rb
index 9e749b0fdfa..468d9899cc7 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started.rb
@@ -16,6 +16,16 @@ module Gitlab
def object_type
MergeRequest
end
+
+ def timestamp_projection
+ mr_metrics_table[:latest_build_started_at]
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def apply_query_customization(query)
+ query.joins(:metrics)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged.rb
index bbfb5d12992..82ecaf1cd6b 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged.rb
@@ -16,6 +16,16 @@ module Gitlab
def object_type
MergeRequest
end
+
+ def timestamp_projection
+ mr_metrics_table[:merged_at]
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def apply_query_customization(query)
+ query.joins(:metrics)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb
index 803317d8b55..7ece7d62faa 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb
@@ -16,6 +16,22 @@ module Gitlab
def object_type
Issue
end
+
+ def timestamp_projection
+ Arel::Nodes::NamedFunction.new('COALESCE', [
+ issue_metrics_table[:first_associated_with_milestone_at],
+ issue_metrics_table[:first_added_to_board_at]
+ ])
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def apply_query_customization(query)
+ query
+ .joins(:metrics)
+ .where(issue_metrics_table[:first_added_to_board_at].not_eq(nil).or(issue_metrics_table[:first_associated_with_milestone_at].not_eq(nil)))
+ .where(issue_metrics_table[:first_mentioned_in_commit_at].not_eq(nil))
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb
new file mode 100644
index 00000000000..607371a32e8
--- /dev/null
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Analytics
+ module CycleAnalytics
+ module StageEvents
+ class ProductionStageEnd < SimpleStageEvent
+ def self.name
+ PlanStageStart.name
+ end
+
+ def self.identifier
+ :production_stage_end
+ end
+
+ def object_type
+ Issue
+ end
+
+ def timestamp_projection
+ mr_metrics_table[:first_deployed_to_production_at]
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def apply_query_customization(query)
+ query.joins(merge_requests_closing_issues: { merge_request: [:metrics] }).where(mr_metrics_table[:first_deployed_to_production_at].gteq(mr_table[:created_at]))
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb
index a55eee048c2..aa392140eb5 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb
@@ -6,6 +6,8 @@ module Gitlab
module StageEvents
# Base class for expressing an event that can be used for a stage.
class StageEvent
+ include Gitlab::CycleAnalytics::MetricsTables
+
def initialize(params)
@params = params
end
@@ -21,6 +23,21 @@ module Gitlab
def object_type
raise NotImplementedError
end
+
+ # Each StageEvent must expose a timestamp or a timestamp like expression in order to build a range query.
+ # Example: get me all the Issue records between start event end end event
+ def timestamp_projection
+ raise NotImplementedError
+ end
+
+ # Optionally a StageEvent may apply additional filtering or join other tables on the base query.
+ def apply_query_customization(query)
+ query
+ end
+
+ private
+
+ attr_reader :params
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d123ac4ee8c..1ad1d0ba547 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8261,7 +8261,7 @@ msgstr ""
msgid "ImportProjects|The repository could not be created."
msgstr ""
-msgid "ImportProjects|Updating the imported projects failed"
+msgid "ImportProjects|Update of imported projects with realtime changes failed"
msgstr ""
msgid "Improve Issue boards"
@@ -10205,7 +10205,7 @@ msgstr ""
msgid "No %{header} for this request."
msgstr ""
-msgid "No %{providerTitle} repositories available to import"
+msgid "No %{providerTitle} repositories found"
msgstr ""
msgid "No Epic"
diff --git a/spec/frontend/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_projects/components/import_projects_table_spec.js
index 17a998d0174..708f2758083 100644
--- a/spec/frontend/import_projects/components/import_projects_table_spec.js
+++ b/spec/frontend/import_projects/components/import_projects_table_spec.js
@@ -93,7 +93,7 @@ describe('ImportProjectsTable', () => {
return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
expect(vm.$el.querySelector('.table')).toBeNull();
- expect(vm.$el.innerText).toMatch(`No ${providerTitle} repositories available to import`);
+ expect(vm.$el.innerText).toMatch(`No ${providerTitle} repositories found`);
});
});
@@ -182,4 +182,10 @@ describe('ImportProjectsTable', () => {
expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
});
});
+
+ it('renders filtering input field', () => {
+ expect(
+ vm.$el.querySelector('input[data-qa-selector="githubish_import_filter_field"]'),
+ ).not.toBeNull();
+ });
});
diff --git a/spec/frontend/import_projects/store/actions_spec.js b/spec/frontend/import_projects/store/actions_spec.js
index 6a7b90788dd..340b6f02d93 100644
--- a/spec/frontend/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_projects/store/actions_spec.js
@@ -97,6 +97,7 @@ describe('import_projects store actions', () => {
describe('fetchRepos', () => {
let mock;
+ const payload = { imported_projects: [{}], provider_repos: [{}], namespaces: [{}] };
beforeEach(() => {
localState.reposPath = `${TEST_HOST}/endpoint.json`;
@@ -105,8 +106,7 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore());
- it('dispatches requestRepos and receiveReposSuccess actions on a successful request', done => {
- const payload = { imported_projects: [{}], provider_repos: [{}], namespaces: [{}] };
+ it('dispatches stopJobsPolling, requestRepos and receiveReposSuccess actions on a successful request', done => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, payload);
testAction(
@@ -115,6 +115,7 @@ describe('import_projects store actions', () => {
localState,
[],
[
+ { type: 'stopJobsPolling' },
{ type: 'requestRepos' },
{
type: 'receiveReposSuccess',
@@ -128,7 +129,7 @@ describe('import_projects store actions', () => {
);
});
- it('dispatches requestRepos and receiveReposSuccess actions on an unsuccessful request', done => {
+ it('dispatches stopJobsPolling, requestRepos and receiveReposError actions on an unsuccessful request', done => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
testAction(
@@ -136,10 +137,39 @@ describe('import_projects store actions', () => {
null,
localState,
[],
- [{ type: 'requestRepos' }, { type: 'receiveReposError' }],
+ [{ type: 'stopJobsPolling' }, { type: 'requestRepos' }, { type: 'receiveReposError' }],
done,
);
});
+
+ describe('when filtered', () => {
+ beforeEach(() => {
+ localState.filter = 'filter';
+ });
+
+ it('fetches repos with filter applied', done => {
+ mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(200, payload);
+
+ testAction(
+ fetchRepos,
+ null,
+ localState,
+ [],
+ [
+ { type: 'stopJobsPolling' },
+ { type: 'requestRepos' },
+ {
+ type: 'receiveReposSuccess',
+ payload: convertObjectPropsToCamelCase(payload, { deep: true }),
+ },
+ {
+ type: 'fetchJobs',
+ },
+ ],
+ done,
+ );
+ });
+ });
});
describe('requestImport', () => {
@@ -249,6 +279,7 @@ describe('import_projects store actions', () => {
describe('fetchJobs', () => {
let mock;
+ const updatedProjects = [{ name: 'imported/project' }, { name: 'provider/repo' }];
beforeEach(() => {
localState.jobsPath = `${TEST_HOST}/endpoint.json`;
@@ -263,7 +294,6 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore());
it('dispatches requestJobs and receiveJobsSuccess actions on a successful request', done => {
- const updatedProjects = [{ name: 'imported/project' }, { name: 'provider/repo' }];
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, updatedProjects);
testAction(
@@ -280,5 +310,29 @@ describe('import_projects store actions', () => {
done,
);
});
+
+ describe('when filtered', () => {
+ beforeEach(() => {
+ localState.filter = 'filter';
+ });
+
+ it('fetches realtime changes with filter applied', done => {
+ mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(200, updatedProjects);
+
+ testAction(
+ fetchJobs,
+ null,
+ localState,
+ [],
+ [
+ {
+ type: 'receiveJobsSuccess',
+ payload: convertObjectPropsToCamelCase(updatedProjects, { deep: true }),
+ },
+ ],
+ done,
+ );
+ });
+ });
});
});
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start_spec.rb
new file mode 100644
index 00000000000..29c8d548754
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::CodeStageStart do
+ let(:subject) { described_class.new({}) }
+ let(:project) { create(:project) }
+
+ it_behaves_like 'cycle analytics event'
+
+ it 'needs connection with an issue via merge_requests_closing_issues table' do
+ issue = create(:issue, project: project)
+ merge_request = create(:merge_request, source_project: project)
+ create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
+
+ other_merge_request = create(:merge_request, source_project: project, source_branch: 'a', target_branch: 'master')
+
+ records = subject.apply_query_customization(MergeRequest.all)
+ expect(records).to eq([merge_request])
+ expect(records).not_to include(other_merge_request)
+ end
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created_spec.rb
new file mode 100644
index 00000000000..efdef91c5a2
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueCreated do
+ it_behaves_like 'cycle analytics event'
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit_spec.rb
new file mode 100644
index 00000000000..50883e1c1e2
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueFirstMentionedInCommit do
+ it_behaves_like 'cycle analytics event'
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end_spec.rb
new file mode 100644
index 00000000000..85062db370a
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueStageEnd do
+ it_behaves_like 'cycle analytics event'
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created_spec.rb
new file mode 100644
index 00000000000..7858b810661
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestCreated do
+ it_behaves_like 'cycle analytics event'
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production_spec.rb
new file mode 100644
index 00000000000..ba9d8be5a2c
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestFirstDeployedToProduction do
+ it_behaves_like 'cycle analytics event'
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished_spec.rb
new file mode 100644
index 00000000000..8e83e10ef96
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestLastBuildFinished do
+ it_behaves_like 'cycle analytics event'
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started_spec.rb
new file mode 100644
index 00000000000..9f6b430a320
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestLastBuildStarted do
+ it_behaves_like 'cycle analytics event'
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged_spec.rb
new file mode 100644
index 00000000000..ce2aa0a60db
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestMerged do
+ it_behaves_like 'cycle analytics event'
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start_spec.rb
new file mode 100644
index 00000000000..cb63139f0a8
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::PlanStageStart do
+ let(:subject) { described_class.new({}) }
+ let(:project) { create(:project) }
+
+ it_behaves_like 'cycle analytics event'
+
+ it 'filters issues where first_associated_with_milestone_at or first_added_to_board_at is filled' do
+ issue1 = create(:issue, project: project)
+ issue1.metrics.update!(first_added_to_board_at: 1.month.ago, first_mentioned_in_commit_at: 2.months.ago)
+
+ issue2 = create(:issue, project: project)
+ issue2.metrics.update!(first_associated_with_milestone_at: 1.month.ago, first_mentioned_in_commit_at: 2.months.ago)
+
+ issue_without_metrics = create(:issue, project: project)
+
+ records = subject.apply_query_customization(Issue.all)
+ expect(records).to match_array([issue1, issue2])
+ expect(records).not_to include(issue_without_metrics)
+ end
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb
index 29f4be76a65..b05faf5d813 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb
@@ -3,8 +3,11 @@
require 'spec_helper'
describe Gitlab::Analytics::CycleAnalytics::StageEvents::StageEvent do
+ let(:instance) { described_class.new({}) }
+
it { expect(described_class).to respond_to(:name) }
it { expect(described_class).to respond_to(:identifier) }
-
- it { expect(described_class.new({})).to respond_to(:object_type) }
+ it { expect(instance).to respond_to(:object_type) }
+ it { expect(instance).to respond_to(:timestamp_projection) }
+ it { expect(instance).to respond_to(:apply_query_customization) }
end
diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb
index 718d9857b18..f23812e7149 100644
--- a/spec/support/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb
@@ -139,6 +139,38 @@ shared_examples 'a GitHub-ish import controller: GET status' do
expect { get :status, format: :json }
.not_to exceed_all_query_limit(control_count)
end
+
+ context 'when filtering' do
+ let(:repo_2) { OpenStruct.new(login: 'emacs', full_name: 'asd/emacs', name: 'emacs', owner: { login: 'owner' }) }
+ let(:project) { create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') }
+ let(:group) { create(:group) }
+
+ before do
+ group.add_owner(user)
+ stub_client(repos: [repo, repo_2, org_repo], orgs: [org], org_repos: [org_repo])
+ end
+
+ it 'filters list of repositories by name' do
+ get :status, params: { filter: 'emacs' }, format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.dig("imported_projects").count).to eq(0)
+ expect(json_response.dig("provider_repos").count).to eq(1)
+ expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_2.id)
+ expect(json_response.dig("namespaces", 0, "id")).to eq(group.id)
+ end
+
+ context 'when user input contains html' do
+ let(:expected_filter) { 'test' }
+ let(:filter) { "<html>#{expected_filter}</html>" }
+
+ it 'sanitizes user input' do
+ get :status, params: { filter: filter }, format: :json
+
+ expect(assigns(:filter)).to eq(expected_filter)
+ end
+ end
+ end
end
shared_examples 'a GitHub-ish import controller: POST create' do
diff --git a/spec/support/shared_examples/cycle_analytics_event_shared_examples.rb b/spec/support/shared_examples/cycle_analytics_event_shared_examples.rb
new file mode 100644
index 00000000000..dce1dbe1cd1
--- /dev/null
+++ b/spec/support/shared_examples/cycle_analytics_event_shared_examples.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+shared_examples_for 'cycle analytics event' do
+ let(:instance) { described_class.new({}) }
+
+ it { expect(described_class.name).to be_a_kind_of(String) }
+ it { expect(described_class.identifier).to be_a_kind_of(Symbol) }
+ it { expect(instance.object_type.ancestors).to include(ApplicationRecord) }
+ it { expect(instance).to respond_to(:timestamp_projection) }
+
+ describe '#apply_query_customization' do
+ it 'expects an ActiveRecord::Relation object as argument and returns a modified version of it' do
+ input_query = instance.object_type.all
+
+ output_query = instance.apply_query_customization(input_query)
+ expect(output_query).to be_a_kind_of(ActiveRecord::Relation)
+ end
+ end
+end