Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
bdbded586b
commit
4584eb0e07
43 changed files with 485 additions and 31 deletions
|
@ -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"
|
||||
|
|
|
@ -38,7 +38,7 @@ export default function mountImportProjectsTable(mountElement) {
|
|||
},
|
||||
|
||||
methods: {
|
||||
...mapActions(['setInitialData']),
|
||||
...mapActions(['setInitialData', 'setFilter']),
|
||||
},
|
||||
|
||||
render(createElement) {
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -12,4 +12,5 @@ export default () => ({
|
|||
isLoadingRepos: false,
|
||||
canSelectNamespace: false,
|
||||
ciCdOnly: false,
|
||||
filter: '',
|
||||
});
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
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.
|
|||
- 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)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -16,6 +16,10 @@ module Gitlab
|
|||
def object_type
|
||||
Issue
|
||||
end
|
||||
|
||||
def timestamp_projection
|
||||
issue_table[:created_at]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -16,6 +16,10 @@ module Gitlab
|
|||
def object_type
|
||||
MergeRequest
|
||||
end
|
||||
|
||||
def timestamp_projection
|
||||
mr_table[:created_at]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in a new issue