Commit 4584eb0e authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent bdbded58
<script> <script>
import _ from 'underscore';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
...@@ -7,6 +8,8 @@ import ImportedProjectTableRow from './imported_project_table_row.vue'; ...@@ -7,6 +8,8 @@ import ImportedProjectTableRow from './imported_project_table_row.vue';
import ProviderRepoTableRow from './provider_repo_table_row.vue'; import ProviderRepoTableRow from './provider_repo_table_row.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
const reposFetchThrottleDelay = 1000;
export default { export default {
name: 'ImportProjectsTable', name: 'ImportProjectsTable',
components: { components: {
...@@ -23,11 +26,11 @@ export default { ...@@ -23,11 +26,11 @@ export default {
}, },
computed: { computed: {
...mapState(['importedProjects', 'providerRepos', 'isLoadingRepos']), ...mapState(['importedProjects', 'providerRepos', 'isLoadingRepos', 'filter']),
...mapGetters(['isImportingAnyRepo', 'hasProviderRepos', 'hasImportedProjects']), ...mapGetters(['isImportingAnyRepo', 'hasProviderRepos', 'hasImportedProjects']),
emptyStateText() { emptyStateText() {
return sprintf(__('No %{providerTitle} repositories available to import'), { return sprintf(__('No %{providerTitle} repositories found'), {
providerTitle: this.providerTitle, providerTitle: this.providerTitle,
}); });
}, },
...@@ -47,21 +50,38 @@ export default { ...@@ -47,21 +50,38 @@ export default {
}, },
methods: { methods: {
...mapActions(['fetchRepos', 'fetchJobs', 'stopJobsPolling', 'clearJobsEtagPoll']), ...mapActions([
'fetchRepos',
'fetchReposFiltered',
'fetchJobs',
'stopJobsPolling',
'clearJobsEtagPoll',
'setFilter',
]),
importAll() { importAll() {
eventHub.$emit('importAll'); eventHub.$emit('importAll');
}, },
handleFilterInput({ target }) {
this.setFilter(target.value);
},
throttledFetchRepos: _.throttle(function fetch() {
eventHub.$off('importAll');
this.fetchRepos();
}, reposFetchThrottleDelay),
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<div class="d-flex justify-content-between align-items-end flex-wrap mb-3"> <p class="light text-nowrap mt-2">
<p class="light text-nowrap mt-2 my-sm-0">
{{ s__('ImportProjects|Select the projects you want to import') }} {{ s__('ImportProjects|Select the projects you want to import') }}
</p> </p>
<div class="d-flex justify-content-between align-items-end flex-wrap mb-3">
<loading-button <loading-button
container-class="btn btn-success js-import-all" container-class="btn btn-success js-import-all"
:loading="isImportingAnyRepo" :loading="isImportingAnyRepo"
...@@ -70,6 +90,19 @@ export default { ...@@ -70,6 +90,19 @@ export default {
type="button" type="button"
@click="importAll" @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> </div>
<gl-loading-icon <gl-loading-icon
v-if="isLoadingRepos" v-if="isLoadingRepos"
......
...@@ -38,7 +38,7 @@ export default function mountImportProjectsTable(mountElement) { ...@@ -38,7 +38,7 @@ export default function mountImportProjectsTable(mountElement) {
}, },
methods: { methods: {
...mapActions(['setInitialData']), ...mapActions(['setInitialData', 'setFilter']),
}, },
render(createElement) { render(createElement) {
......
...@@ -5,6 +5,7 @@ import Poll from '~/lib/utils/poll'; ...@@ -5,6 +5,7 @@ import Poll from '~/lib/utils/poll';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { jobsPathWithFilter, reposPathWithFilter } from './getters';
let eTagPoll; let eTagPoll;
...@@ -19,16 +20,20 @@ export const restartJobsPolling = () => { ...@@ -19,16 +20,20 @@ export const restartJobsPolling = () => {
}; };
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); 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 requestRepos = ({ commit }, repos) => commit(types.REQUEST_REPOS, repos);
export const receiveReposSuccess = ({ commit }, repos) => export const receiveReposSuccess = ({ commit }, repos) =>
commit(types.RECEIVE_REPOS_SUCCESS, repos); commit(types.RECEIVE_REPOS_SUCCESS, repos);
export const receiveReposError = ({ commit }) => commit(types.RECEIVE_REPOS_ERROR); export const receiveReposError = ({ commit }) => commit(types.RECEIVE_REPOS_ERROR);
export const fetchRepos = ({ state, dispatch }) => { export const fetchRepos = ({ state, dispatch }) => {
dispatch('stopJobsPolling');
dispatch('requestRepos'); dispatch('requestRepos');
const { provider } = state;
return axios return axios
.get(state.reposPath) .get(reposPathWithFilter(state))
.then(({ data }) => .then(({ data }) =>
dispatch('receiveReposSuccess', convertObjectPropsToCamelCase(data, { deep: true })), dispatch('receiveReposSuccess', convertObjectPropsToCamelCase(data, { deep: true })),
) )
...@@ -36,7 +41,7 @@ export const fetchRepos = ({ state, dispatch }) => { ...@@ -36,7 +41,7 @@ export const fetchRepos = ({ state, dispatch }) => {
.catch(() => { .catch(() => {
createFlash( createFlash(
sprintf(s__('ImportProjects|Requesting your %{provider} repositories failed'), { sprintf(s__('ImportProjects|Requesting your %{provider} repositories failed'), {
provider: state.provider, provider,
}), }),
); );
...@@ -77,16 +82,23 @@ export const fetchImport = ({ state, dispatch }, { newName, targetNamespace, rep ...@@ -77,16 +82,23 @@ export const fetchImport = ({ state, dispatch }, { newName, targetNamespace, rep
export const receiveJobsSuccess = ({ commit }, updatedProjects) => export const receiveJobsSuccess = ({ commit }, updatedProjects) =>
commit(types.RECEIVE_JOBS_SUCCESS, updatedProjects); commit(types.RECEIVE_JOBS_SUCCESS, updatedProjects);
export const fetchJobs = ({ state, dispatch }) => { export const fetchJobs = ({ state, dispatch }) => {
if (eTagPoll) return; const { filter } = state;
if (eTagPoll) {
stopJobsPolling();
clearJobsEtagPoll();
}
eTagPoll = new Poll({ eTagPoll = new Poll({
resource: { resource: {
fetchJobs: () => axios.get(state.jobsPath), fetchJobs: () => axios.get(jobsPathWithFilter(state)),
}, },
method: 'fetchJobs', method: 'fetchJobs',
successCallback: ({ data }) => successCallback: ({ data }) =>
dispatch('receiveJobsSuccess', convertObjectPropsToCamelCase(data, { deep: true })), 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()) { if (!Visibility.hidden()) {
......
...@@ -20,3 +20,8 @@ export const isImportingAnyRepo = state => state.reposBeingImported.length > 0; ...@@ -20,3 +20,8 @@ export const isImportingAnyRepo = state => state.reposBeingImported.length > 0;
export const hasProviderRepos = state => state.providerRepos.length > 0; export const hasProviderRepos = state => state.providerRepos.length > 0;
export const hasImportedProjects = state => state.importedProjects.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;
...@@ -9,3 +9,5 @@ export const RECEIVE_IMPORT_SUCCESS = 'RECEIVE_IMPORT_SUCCESS'; ...@@ -9,3 +9,5 @@ export const RECEIVE_IMPORT_SUCCESS = 'RECEIVE_IMPORT_SUCCESS';
export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR'; export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR';
export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS'; export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
export const SET_FILTER = 'SET_FILTER';
...@@ -6,6 +6,10 @@ export default { ...@@ -6,6 +6,10 @@ export default {
Object.assign(state, data); Object.assign(state, data);
}, },
[types.SET_FILTER](state, filter) {
state.filter = filter;
},
[types.REQUEST_REPOS](state) { [types.REQUEST_REPOS](state) {
state.isLoadingRepos = true; state.isLoadingRepos = true;
}, },
......
...@@ -12,4 +12,5 @@ export default () => ({ ...@@ -12,4 +12,5 @@ export default () => ({
isLoadingRepos: false, isLoadingRepos: false,
canSelectNamespace: false, canSelectNamespace: false,
ciCdOnly: false, ciCdOnly: false,
filter: '',
}); });
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
class Import::GithubController < Import::BaseController class Import::GithubController < Import::BaseController
include ImportHelper include ImportHelper
include ActionView::Helpers::SanitizeHelper
before_action :verify_import_enabled before_action :verify_import_enabled
before_action :provider_auth, only: [:status, :realtime_changes, :create] before_action :provider_auth, only: [:status, :realtime_changes, :create]
...@@ -55,7 +56,7 @@ class Import::GithubController < Import::BaseController ...@@ -55,7 +56,7 @@ class Import::GithubController < Import::BaseController
def realtime_changes def realtime_changes
Gitlab::PollingInterval.set_header(response, interval: 3_000) 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 end
private private
...@@ -82,7 +83,7 @@ class Import::GithubController < Import::BaseController ...@@ -82,7 +83,7 @@ class Import::GithubController < Import::BaseController
end end
def already_added_projects def already_added_projects
@already_added_projects ||= find_already_added_projects(provider) @already_added_projects ||= filtered(find_already_added_projects(provider))
end end
def already_added_project_names def already_added_project_names
...@@ -104,7 +105,7 @@ class Import::GithubController < Import::BaseController ...@@ -104,7 +105,7 @@ class Import::GithubController < Import::BaseController
end end
def client_repos def client_repos
@client_repos ||= client.repos @client_repos ||= filtered(client.repos)
end end
def verify_import_enabled def verify_import_enabled
...@@ -185,6 +186,20 @@ class Import::GithubController < Import::BaseController ...@@ -185,6 +186,20 @@ class Import::GithubController < Import::BaseController
def extra_import_params def extra_import_params
{} {}
end 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 end
Import::GithubController.prepend_if_ee('EE::Import::GithubController') Import::GithubController.prepend_if_ee('EE::Import::GithubController')
---
title: Add GitHub & Gitea importers project filtering
merge_request: 16823
author:
type: added
...@@ -66,10 +66,14 @@ From there, you can see the import statuses of your Gitea repositories. ...@@ -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 - whereas those that are not yet imported will have an **Import** button on the
right side of the table. right side of the table.
If you want, you can import all your Gitea projects in one go by hitting You also can:
**Import all projects** in the upper left corner.
![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)
--- ---
......
...@@ -115,11 +115,14 @@ your GitHub repositories are listed. ...@@ -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, 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. 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 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. 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. 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 ## Mirroring and pipeline status sharing
Depending your GitLab tier, [project mirroring](../../../workflow/repository_mirroring.md) can be set up to keep Depending your GitLab tier, [project mirroring](../../../workflow/repository_mirroring.md) can be set up to keep
......
...@@ -18,7 +18,8 @@ module Gitlab ...@@ -18,7 +18,8 @@ module Gitlab
StageEvents::MergeRequestMerged => 104, StageEvents::MergeRequestMerged => 104,
StageEvents::CodeStageStart => 1_000, StageEvents::CodeStageStart => 1_000,
StageEvents::IssueStageEnd => 1_001, StageEvents::IssueStageEnd => 1_001,
StageEvents::PlanStageStart => 1_002 StageEvents::PlanStageStart => 1_002,
StageEvents::ProductionStageEnd => 1_003
}.freeze }.freeze
EVENTS = ENUM_MAPPING.keys.freeze EVENTS = ENUM_MAPPING.keys.freeze
...@@ -32,7 +33,8 @@ module Gitlab ...@@ -32,7 +33,8 @@ module Gitlab
StageEvents::MergeRequestCreated StageEvents::MergeRequestCreated
], ],
StageEvents::IssueCreated => [ StageEvents::IssueCreated => [
StageEvents::IssueStageEnd StageEvents::IssueStageEnd,
StageEvents::ProductionStageEnd
], ],
StageEvents::MergeRequestCreated => [ StageEvents::MergeRequestCreated => [
StageEvents::MergeRequestMerged StageEvents::MergeRequestMerged
......
...@@ -16,6 +16,21 @@ module Gitlab ...@@ -16,6 +16,21 @@ module Gitlab
def object_type def object_type
MergeRequest MergeRequest
end 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 end
end end
......
...@@ -16,6 +16,10 @@ module Gitlab ...@@ -16,6 +16,10 @@ module Gitlab
def object_type def object_type
Issue Issue
end end
def timestamp_projection
issue_table[:created_at]
end
end end
end end
end end
......
...@@ -16,6 +16,16 @@ module Gitlab ...@@ -16,6 +16,16 @@ module Gitlab
def object_type def object_type
Issue Issue
end 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 end
end end
......
...@@ -16,6 +16,19 @@ module Gitlab ...@@ -16,6 +16,19 @@ module Gitlab
def object_type def object_type
Issue Issue
end 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 end
end end
......
...@@ -16,6 +16,10 @@ module Gitlab ...@@ -16,6 +16,10 @@ module Gitlab
def object_type def object_type
MergeRequest MergeRequest
end end
def timestamp_projection
mr_table[:created_at]
end
end end
end end
end end
......
...@@ -16,6 +16,16 @@ module Gitlab ...@@ -16,6 +16,16 @@ module Gitlab
def object_type def object_type
MergeRequest MergeRequest
end 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 end
end end
......
...@@ -16,6 +16,16 @@ module Gitlab ...@@ -16,6 +16,16 @@ module Gitlab
def object_type def object_type
MergeRequest MergeRequest
end 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 end
end end
......
...@@ -16,6 +16,16 @@ module Gitlab ...@@ -16,6 +16,16 @@ module Gitlab
def object_type def object_type
MergeRequest MergeRequest
end 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 end
end end
......
...@@ -16,6 +16,16 @@ module Gitlab ...@@ -16,6 +16,16 @@ module Gitlab
def object_type def object_type
MergeRequest MergeRequest
end 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 end
end end
......
...@@ -16,6 +16,22 @@ module Gitlab ...@@ -16,6 +16,22 @@ module Gitlab
def object_type def object_type
Issue Issue
end 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 end
end end
......
# 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
...@@ -6,6 +6,8 @@ module Gitlab ...@@ -6,6 +6,8 @@ module Gitlab
module StageEvents module StageEvents
# Base class for expressing an event that can be used for a stage. # Base class for expressing an event that can be used for a stage.
class StageEvent class StageEvent
include Gitlab::CycleAnalytics::MetricsTables
def initialize(params) def initialize(params)
@params = params @params = params
end end
...@@ -21,6 +23,21 @@ module Gitlab ...@@ -21,6 +23,21 @@ module Gitlab
def object_type def object_type
raise NotImplementedError raise NotImplementedError
end 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 end
end end
......
...@@ -8261,7 +8261,7 @@ msgstr "" ...@@ -8261,7 +8261,7 @@ msgstr ""
msgid "ImportProjects|The repository could not be created." msgid "ImportProjects|The repository could not be created."
msgstr "" msgstr ""
msgid "ImportProjects|Updating the imported projects failed" msgid "ImportProjects|Update of imported projects with realtime changes failed"
msgstr "" msgstr ""
msgid "Improve Issue boards" msgid "Improve Issue boards"
...@@ -10205,7 +10205,7 @@ msgstr "" ...@@ -10205,7 +10205,7 @@ msgstr ""
msgid "No %{header} for this request." msgid "No %{header} for this request."
msgstr "" msgstr ""
msgid "No %{providerTitle} repositories available to import" msgid "No %{providerTitle} repositories found"
msgstr "" msgstr ""
msgid "No Epic" msgid "No Epic"
......
...@@ -93,7 +93,7 @@ describe('ImportProjectsTable', () => { ...@@ -93,7 +93,7 @@ describe('ImportProjectsTable', () => {
return vm.$nextTick().then(() => { return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull(); expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
expect(vm.$el.querySelector('.table')).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', () => { ...@@ -182,4 +182,10 @@ describe('ImportProjectsTable', () => {
expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull(); 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();
});
}); });
...@@ -97,6 +97,7 @@ describe('import_projects store actions', () => { ...@@ -97,6 +97,7 @@ describe('import_projects store actions', () => {
describe('fetchRepos', () => { describe('fetchRepos', () => {
let mock; let mock;
const payload = { imported_projects: [{}], provider_repos: [{}], namespaces: [{}] };
beforeEach(() => { beforeEach(() => {
localState.reposPath = `${TEST_HOST}/endpoint.json`; localState.reposPath = `${TEST_HOST}/endpoint.json`;
...@@ -105,8 +106,7 @@ describe('import_projects store actions', () => { ...@@ -105,8 +106,7 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore()); afterEach(() => mock.restore());
it('dispatches requestRepos and receiveReposSuccess actions on a successful request', done => { it('dispatches stopJobsPolling, requestRepos and receiveReposSuccess actions on a successful request', done => {
const payload = { imported_projects: [{}], provider_repos: [{}], namespaces: [{}] };
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, payload); mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, payload);
testAction( testAction(
...@@ -115,6 +115,7 @@ describe('import_projects store actions', () => { ...@@ -115,6 +115,7 @@ describe('import_projects store actions', () => {
localState, localState,
[], [],
[ [
{ type: 'stopJobsPolling' },
{ type: 'requestRepos' }, { type: 'requestRepos' },
{ {
type: 'receiveReposSuccess', type: 'receiveReposSuccess',
...@@ -128,7 +129,7 @@ describe('import_projects store actions', () => { ...@@ -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); mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
testAction( testAction(
...@@ -136,11 +137,40 @@ describe('import_projects store actions', () => { ...@@ -136,11 +137,40 @@ describe('import_projects store actions', () => {
null, null,
localState, 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, done,
); );
}); });
}); });
});
describe('requestImport', () => { describe('requestImport', () => {
it(`commits ${REQUEST_IMPORT} mutation`, done => { it(`commits ${REQUEST_IMPORT} mutation`, done => {
...@@ -249,6 +279,7 @@ describe('import_projects store actions', () => { ...@@ -249,6 +279,7 @@ describe('import_projects store actions', () => {
describe('fetchJobs', () => { describe('fetchJobs', () => {
let mock; let mock;
const updatedProjects = [{ name: 'imported/project' }, { name: 'provider/repo' }];
beforeEach(() => { beforeEach(() => {
localState.jobsPath = `${TEST_HOST}/endpoint.json`; localState.jobsPath = `${TEST_HOST}/endpoint.json`;
...@@ -263,7 +294,6 @@ describe('import_projects store actions', () => { ...@@ -263,7 +294,6 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore()); afterEach(() => mock.restore());
it('dispatches requestJobs and receiveJobsSuccess actions on a successful request', done => { 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); mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, updatedProjects);
testAction( testAction(
...@@ -280,5 +310,29 @@ describe('import_projects store actions', () => { ...@@ -280,5 +310,29 @@ describe('import_projects store actions', () => {
done, 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,
);
});
});
}); });
}); });
# 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
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueCreated do
it_behaves_like 'cycle analytics event'
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueFirstMentionedInCommit do
it_behaves_like 'cycle analytics event'
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueStageEnd do
it_behaves_like 'cycle analytics event'
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestCreated do
it_behaves_like 'cycle analytics event'
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestFirstDeployedToProduction do
it_behaves_like 'cycle analytics event'
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestLastBuildFinished do
it_behaves_like 'cycle analytics event'
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestLastBuildStarted do
it_behaves_like 'cycle analytics event'
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestMerged do
it_behaves_like 'cycle analytics event'
end
# 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
...@@ -3,8 +3,11 @@ ...@@ -3,8 +3,11 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::Analytics::CycleAnalytics::StageEvents::StageEvent do 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(:name) }
it { expect(described_class).to respond_to(:identifier) } it { expect(described_class).to respond_to(:identifier) }
it { expect(instance).to respond_to(:object_type) }
it { expect(described_class.new({})).to respond_to(:object_type) } it { expect(instance).to respond_to(:timestamp_projection) }
it { expect(instance).to respond_to(:apply_query_customization) }
end end
...@@ -139,6 +139,38 @@ shared_examples 'a GitHub-ish import controller: GET status' do ...@@ -139,6 +139,38 @@ shared_examples 'a GitHub-ish import controller: GET status' do
expect { get :status, format: :json } expect { get :status, format: :json }
.not_to exceed_all_query_limit(control_count) .not_to exceed_all_query_limit(control_count)
end 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 end
shared_examples 'a GitHub-ish import controller: POST create' do shared_examples 'a GitHub-ish import controller: POST create' do
......
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment