Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2019-09-18 15:06:03 +00:00
parent bdbded586b
commit 4584eb0e07
43 changed files with 485 additions and 31 deletions

View File

@ -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"

View File

@ -38,7 +38,7 @@ export default function mountImportProjectsTable(mountElement) {
},
methods: {
...mapActions(['setInitialData']),
...mapActions(['setInitialData', 'setFilter']),
},
render(createElement) {

View File

@ -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()) {

View File

@ -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;

View File

@ -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';

View File

@ -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;
},

View File

@ -12,4 +12,5 @@ export default () => ({
isLoadingRepos: false,
canSelectNamespace: false,
ciCdOnly: false,
filter: '',
});

View File

@ -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')

View File

@ -0,0 +1,5 @@
---
title: Add GitHub & Gitea importers project filtering
merge_request: 16823
author:
type: added

View File

@ -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)
---

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -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

View File

@ -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

View File

@ -16,6 +16,10 @@ module Gitlab
def object_type
Issue
end
def timestamp_projection
issue_table[:created_at]
end
end
end
end

View File

@ -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

View File

@ -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

View File

@ -16,6 +16,10 @@ module Gitlab
def object_type
MergeRequest
end
def timestamp_projection
mr_table[:created_at]
end
end
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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();
});
});

View File

@ -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,
);
});
});
});
});

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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