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>2020-06-08 15:08:26 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-06-08 15:08:26 +0300
commitf149549c3432ffb179f6904e4ba0ea64027202d0 (patch)
tree9c6dca1e76c0de43e5e5f6b5d34c5616abbc7501
parentb001207ce2033589373cd7558ca69c4e5732ce6b (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/import_projects/components/import_projects_table.vue60
-rw-r--r--app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue30
-rw-r--r--app/assets/javascripts/import_projects/components/provider_repo_table_row.vue6
-rw-r--r--app/assets/javascripts/import_projects/index.js51
-rw-r--r--app/assets/javascripts/import_projects/store/actions.js35
-rw-r--r--app/assets/javascripts/import_projects/store/getters.js2
-rw-r--r--app/assets/javascripts/import_projects/store/index.js4
-rw-r--r--app/assets/javascripts/import_projects/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/import_projects/store/mutations.js10
-rw-r--r--app/assets/javascripts/import_projects/store/state.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue6
-rw-r--r--app/graphql/types/metrics/dashboard_type.rb3
-rw-r--r--app/models/ci/job_artifact.rb10
-rw-r--r--app/models/performance_monitoring/prometheus_dashboard.rb29
-rw-r--r--app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml2
-rw-r--r--app/views/projects/_zen.html.haml4
-rw-r--r--changelogs/unreleased/mwaw-210289-add-metrics-dashboard-validation-to-grapql.yml5
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/post_migrate/20200518114540_schedule_fix_ruby_object_in_audit_events.rb41
-rw-r--r--db/structure.sql3
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql63
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json210
-rw-r--r--doc/api/graphql/reference/index.md11
-rw-r--r--doc/update/upgrading_from_source.md12
-rw-r--r--doc/user/application_security/threat_monitoring/index.md45
-rw-r--r--doc/user/project/code_owners.md8
-rw-r--r--doc/user/project/integrations/prometheus.md2
-rw-r--r--lib/gitlab/background_migration/fix_ruby_object_in_audit_events.rb13
-rw-r--r--lib/gitlab/ci/config/entry/reports.rb4
-rw-r--r--locale/gitlab.pot24
-rw-r--r--package.json2
-rw-r--r--qa/qa/page/project/settings/ci_cd.rb2
-rw-r--r--spec/frontend/import_projects/components/import_projects_table_spec.js272
-rw-r--r--spec/frontend/import_projects/components/provider_repo_table_row_spec.js11
-rw-r--r--spec/frontend/import_projects/store/actions_spec.js189
-rw-r--r--spec/frontend/import_projects/store/getters_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/deprecated_modal_2_spec.js (renamed from spec/javascripts/vue_shared/components/deprecated_modal_2_spec.js)31
-rw-r--r--spec/frontend/vue_shared/components/deprecated_modal_spec.js (renamed from spec/javascripts/vue_shared/components/deprecated_modal_spec.js)10
-rw-r--r--spec/frontend/vue_shared/directives/tooltip_spec.js98
-rw-r--r--spec/frontend/vue_shared/translate_spec.js214
-rw-r--r--spec/graphql/types/metrics/dashboard_type_spec.rb2
-rw-r--r--spec/javascripts/vue_shared/directives/tooltip_spec.js89
-rw-r--r--spec/javascripts/vue_shared/translate_spec.js251
-rw-r--r--spec/models/performance_monitoring/prometheus_dashboard_spec.rb39
-rw-r--r--spec/models/project_spec.rb2
-rw-r--r--spec/requests/api/graphql/metrics/dashboard_query_spec.rb56
-rw-r--r--spec/serializers/provider_repo_serializer_spec.rb9
-rw-r--r--spec/services/ci/retry_build_service_spec.rb3
-rw-r--r--yarn.lock8
49 files changed, 1191 insertions, 810 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 849bda28d03..f2ed16ba59f 100644
--- a/app/assets/javascripts/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_projects/components/import_projects_table.vue
@@ -1,11 +1,11 @@
<script>
import { throttle } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import ImportedProjectTableRow from './imported_project_table_row.vue';
import ProviderRepoTableRow from './provider_repo_table_row.vue';
+import IncompatibleRepoTableRow from './incompatible_repo_table_row.vue';
import eventHub from '../event_hub';
const reposFetchThrottleDelay = 1000;
@@ -15,8 +15,9 @@ export default {
components: {
ImportedProjectTableRow,
ProviderRepoTableRow,
- LoadingButton,
+ IncompatibleRepoTableRow,
GlLoadingIcon,
+ GlButton,
},
props: {
providerTitle: {
@@ -26,8 +27,25 @@ export default {
},
computed: {
- ...mapState(['importedProjects', 'providerRepos', 'isLoadingRepos', 'filter']),
- ...mapGetters(['isImportingAnyRepo', 'hasProviderRepos', 'hasImportedProjects']),
+ ...mapState([
+ 'importedProjects',
+ 'providerRepos',
+ 'incompatibleRepos',
+ 'isLoadingRepos',
+ 'filter',
+ ]),
+ ...mapGetters([
+ 'isImportingAnyRepo',
+ 'hasProviderRepos',
+ 'hasImportedProjects',
+ 'hasIncompatibleRepos',
+ ]),
+
+ importAllButtonText() {
+ return this.hasIncompatibleRepos
+ ? __('Import all compatible repositories')
+ : __('Import all repositories');
+ },
emptyStateText() {
return sprintf(__('No %{providerTitle} repositories found'), {
@@ -68,7 +86,6 @@ export default {
},
throttledFetchRepos: throttle(function fetch() {
- eventHub.$off('importAll');
this.fetchRepos();
}, reposFetchThrottleDelay),
},
@@ -80,17 +97,24 @@ export default {
<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">
- <loading-button
- container-class="btn btn-success js-import-all"
+ <template v-if="hasIncompatibleRepos">
+ <slot name="incompatible-repos-warning"> </slot>
+ </template>
+ <div
+ v-if="!isLoadingRepos"
+ class="d-flex justify-content-between align-items-end flex-wrap mb-3"
+ >
+ <gl-button
+ variant="success"
:loading="isImportingAnyRepo"
- :label="__('Import all repositories')"
:disabled="!hasProviderRepos"
type="button"
@click="importAll"
- />
- <form novalidate @submit.prevent>
+ >
+ {{ importAllButtonText }}
+ </gl-button>
+ <slot name="actions"></slot>
+ <form class="gl-ml-auto" novalidate @submit.prevent>
<input
:value="filter"
data-qa-selector="githubish_import_filter_field"
@@ -109,7 +133,10 @@ export default {
class="js-loading-button-icon import-projects-loading-icon"
size="md"
/>
- <div v-else-if="hasProviderRepos || hasImportedProjects" class="table-responsive">
+ <div
+ v-else-if="hasProviderRepos || hasImportedProjects || hasIncompatibleRepos"
+ class="table-responsive"
+ >
<table class="table import-table">
<thead>
<th class="import-jobs-from-col">{{ fromHeaderText }}</th>
@@ -124,6 +151,11 @@ export default {
:project="project"
/>
<provider-repo-table-row v-for="repo in providerRepos" :key="repo.id" :repo="repo" />
+ <incompatible-repo-table-row
+ v-for="repo in incompatibleRepos"
+ :key="repo.id"
+ :repo="repo"
+ />
</tbody>
</table>
</div>
diff --git a/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue b/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue
new file mode 100644
index 00000000000..fa2fb439eac
--- /dev/null
+++ b/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue
@@ -0,0 +1,30 @@
+<script>
+import { GlBadge } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlBadge,
+ },
+ props: {
+ repo: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <tr class="import-row">
+ <td>
+ <a :href="repo.providerLink" rel="noreferrer noopener" target="_blank">
+ {{ repo.fullName }}
+ </a>
+ </td>
+ <td></td>
+ <td></td>
+ <td>
+ <gl-badge variant="danger">{{ __('Incompatible project') }}</gl-badge>
+ </td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
index 6e227ab3d82..63524d61146 100644
--- a/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
@@ -53,7 +53,11 @@ export default {
},
created() {
- eventHub.$on('importAll', () => this.importRepo());
+ eventHub.$on('importAll', this.importRepo);
+ },
+
+ beforeDestroy() {
+ eventHub.$off('importAll', this.importRepo);
},
methods: {
diff --git a/app/assets/javascripts/import_projects/index.js b/app/assets/javascripts/import_projects/index.js
index b069dcb7766..49d38b56e40 100644
--- a/app/assets/javascripts/import_projects/index.js
+++ b/app/assets/javascripts/import_projects/index.js
@@ -1,5 +1,4 @@
import Vue from 'vue';
-import { mapActions } from 'vuex';
import Translate from '../vue_shared/translate';
import ImportProjectsTable from './components/import_projects_table.vue';
import { parseBoolean } from '../lib/utils/common_utils';
@@ -7,42 +6,44 @@ import createStore from './store';
Vue.use(Translate);
-export default function mountImportProjectsTable(mountElement) {
- if (!mountElement) return undefined;
-
+export function initStoreFromElement(element) {
const {
reposPath,
provider,
- providerTitle,
canSelectNamespace,
jobsPath,
importPath,
ciCdOnly,
- } = mountElement.dataset;
+ } = element.dataset;
- const store = createStore();
- return new Vue({
- el: mountElement,
- store,
+ return createStore({
+ reposPath,
+ provider,
+ jobsPath,
+ importPath,
+ defaultTargetNamespace: gon.current_username,
+ ciCdOnly: parseBoolean(ciCdOnly),
+ canSelectNamespace: parseBoolean(canSelectNamespace),
+ });
+}
- created() {
- this.setInitialData({
- reposPath,
- provider,
- jobsPath,
- importPath,
- defaultTargetNamespace: gon.current_username,
- ciCdOnly: parseBoolean(ciCdOnly),
- canSelectNamespace: parseBoolean(canSelectNamespace),
- });
- },
+export function initPropsFromElement(element) {
+ return {
+ providerTitle: element.dataset.providerTitle,
+ };
+}
- methods: {
- ...mapActions(['setInitialData', 'setFilter']),
- },
+export default function mountImportProjectsTable(mountElement) {
+ if (!mountElement) return undefined;
+
+ const store = initStoreFromElement(mountElement);
+ const props = initPropsFromElement(mountElement);
+ return new Vue({
+ el: mountElement,
+ store,
render(createElement) {
- return createElement(ImportProjectsTable, { props: { providerTitle } });
+ return createElement(ImportProjectsTable, { props });
},
});
}
diff --git a/app/assets/javascripts/import_projects/store/actions.js b/app/assets/javascripts/import_projects/store/actions.js
index 0fb9a4cdfd4..6cf71126882 100644
--- a/app/assets/javascripts/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_projects/store/actions.js
@@ -19,23 +19,18 @@ export const restartJobsPolling = () => {
if (eTagPoll) eTagPoll.restart();
};
-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 }) => {
+export const fetchRepos = ({ state, dispatch, commit }) => {
dispatch('stopJobsPolling');
- dispatch('requestRepos');
+ commit(types.REQUEST_REPOS);
const { provider } = state;
return axios
.get(reposPathWithFilter(state))
.then(({ data }) =>
- dispatch('receiveReposSuccess', convertObjectPropsToCamelCase(data, { deep: true })),
+ commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })),
)
.then(() => dispatch('fetchJobs'))
.catch(() => {
@@ -45,19 +40,14 @@ export const fetchRepos = ({ state, dispatch }) => {
}),
);
- dispatch('receiveReposError');
+ commit(types.RECEIVE_REPOS_ERROR);
});
};
-export const requestImport = ({ commit, state }, repoId) => {
- if (!state.reposBeingImported.includes(repoId)) commit(types.REQUEST_IMPORT, repoId);
-};
-export const receiveImportSuccess = ({ commit }, { importedProject, repoId }) =>
- commit(types.RECEIVE_IMPORT_SUCCESS, { importedProject, repoId });
-export const receiveImportError = ({ commit }, repoId) =>
- commit(types.RECEIVE_IMPORT_ERROR, repoId);
-export const fetchImport = ({ state, dispatch }, { newName, targetNamespace, repo }) => {
- dispatch('requestImport', repo.id);
+export const fetchImport = ({ state, commit }, { newName, targetNamespace, repo }) => {
+ if (!state.reposBeingImported.includes(repo.id)) {
+ commit(types.REQUEST_IMPORT, repo.id);
+ }
return axios
.post(state.importPath, {
@@ -67,7 +57,7 @@ export const fetchImport = ({ state, dispatch }, { newName, targetNamespace, rep
target_namespace: targetNamespace,
})
.then(({ data }) =>
- dispatch('receiveImportSuccess', {
+ commit(types.RECEIVE_IMPORT_SUCCESS, {
importedProject: convertObjectPropsToCamelCase(data, { deep: true }),
repoId: repo.id,
}),
@@ -75,13 +65,14 @@ export const fetchImport = ({ state, dispatch }, { newName, targetNamespace, rep
.catch(() => {
createFlash(s__('ImportProjects|Importing the project failed'));
- dispatch('receiveImportError', { repoId: repo.id });
+ commit(types.RECEIVE_IMPORT_ERROR, repo.id);
});
};
export const receiveJobsSuccess = ({ commit }, updatedProjects) =>
commit(types.RECEIVE_JOBS_SUCCESS, updatedProjects);
-export const fetchJobs = ({ state, dispatch }) => {
+
+export const fetchJobs = ({ state, commit, dispatch }) => {
const { filter } = state;
if (eTagPoll) {
@@ -95,7 +86,7 @@ export const fetchJobs = ({ state, dispatch }) => {
},
method: 'fetchJobs',
successCallback: ({ data }) =>
- dispatch('receiveJobsSuccess', convertObjectPropsToCamelCase(data, { deep: true })),
+ commit(types.RECEIVE_JOBS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })),
errorCallback: () =>
createFlash(s__('ImportProjects|Update of imported projects with realtime changes failed')),
data: { filter },
diff --git a/app/assets/javascripts/import_projects/store/getters.js b/app/assets/javascripts/import_projects/store/getters.js
index b107c293181..e6eb8f523de 100644
--- a/app/assets/javascripts/import_projects/store/getters.js
+++ b/app/assets/javascripts/import_projects/store/getters.js
@@ -21,6 +21,8 @@ export const hasProviderRepos = state => state.providerRepos.length > 0;
export const hasImportedProjects = state => state.importedProjects.length > 0;
+export const hasIncompatibleRepos = state => state.incompatibleRepos.length > 0;
+
export const reposPathWithFilter = ({ reposPath, filter = '' }) =>
filter ? `${reposPath}?filter=${filter}` : reposPath;
export const jobsPathWithFilter = ({ jobsPath, filter = '' }) =>
diff --git a/app/assets/javascripts/import_projects/store/index.js b/app/assets/javascripts/import_projects/store/index.js
index ff1fd1e598e..29deb7868ba 100644
--- a/app/assets/javascripts/import_projects/store/index.js
+++ b/app/assets/javascripts/import_projects/store/index.js
@@ -9,9 +9,9 @@ Vue.use(Vuex);
export { state, actions, getters, mutations };
-export default () =>
+export default initialState =>
new Vuex.Store({
- state: state(),
+ state: { ...state(), ...initialState },
actions,
mutations,
getters,
diff --git a/app/assets/javascripts/import_projects/store/mutation_types.js b/app/assets/javascripts/import_projects/store/mutation_types.js
index 16574f4450f..a23b7eef986 100644
--- a/app/assets/javascripts/import_projects/store/mutation_types.js
+++ b/app/assets/javascripts/import_projects/store/mutation_types.js
@@ -1,5 +1,3 @@
-export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
-
export const REQUEST_REPOS = 'REQUEST_REPOS';
export const RECEIVE_REPOS_SUCCESS = 'RECEIVE_REPOS_SUCCESS';
export const RECEIVE_REPOS_ERROR = 'RECEIVE_REPOS_ERROR';
diff --git a/app/assets/javascripts/import_projects/store/mutations.js b/app/assets/javascripts/import_projects/store/mutations.js
index 6c56cfa8298..ec62d0640ef 100644
--- a/app/assets/javascripts/import_projects/store/mutations.js
+++ b/app/assets/javascripts/import_projects/store/mutations.js
@@ -2,10 +2,6 @@ import Vue from 'vue';
import * as types from './mutation_types';
export default {
- [types.SET_INITIAL_DATA](state, data) {
- Object.assign(state, data);
- },
-
[types.SET_FILTER](state, filter) {
state.filter = filter;
},
@@ -14,11 +10,15 @@ export default {
state.isLoadingRepos = true;
},
- [types.RECEIVE_REPOS_SUCCESS](state, { importedProjects, providerRepos, namespaces }) {
+ [types.RECEIVE_REPOS_SUCCESS](
+ state,
+ { importedProjects, providerRepos, incompatibleRepos, namespaces },
+ ) {
state.isLoadingRepos = false;
state.importedProjects = importedProjects;
state.providerRepos = providerRepos;
+ state.incompatibleRepos = incompatibleRepos ?? [];
state.namespaces = namespaces;
},
diff --git a/app/assets/javascripts/import_projects/store/state.js b/app/assets/javascripts/import_projects/store/state.js
index 829f3aa4fbb..0418d735b1d 100644
--- a/app/assets/javascripts/import_projects/store/state.js
+++ b/app/assets/javascripts/import_projects/store/state.js
@@ -7,6 +7,7 @@ export default () => ({
currentUsername: '',
importedProjects: [],
providerRepos: [],
+ incompatibleRepos: [],
namespaces: [],
reposBeingImported: [],
isLoadingRepos: false,
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 8007ccb91d5..893b32606f8 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -245,11 +245,11 @@ export default {
<div class="zen-backdrop">
<slot name="textarea"></slot>
<a
- class="zen-control zen-control-leave js-zen-leave"
+ class="zen-control zen-control-leave js-zen-leave gl-text-gray-700"
href="#"
- :aria-label="__('Enter zen mode')"
+ :aria-label="__('Leave zen mode')"
>
- <icon :size="32" name="screen-normal" />
+ <icon :size="16" name="screen-normal" />
</a>
<markdown-toolbar
:markdown-docs-path="markdownDocsPath"
diff --git a/app/graphql/types/metrics/dashboard_type.rb b/app/graphql/types/metrics/dashboard_type.rb
index d684533ff94..bbcce2d9596 100644
--- a/app/graphql/types/metrics/dashboard_type.rb
+++ b/app/graphql/types/metrics/dashboard_type.rb
@@ -10,6 +10,9 @@ module Types
field :path, GraphQL::STRING_TYPE, null: true,
description: 'Path to a file with the dashboard definition'
+ field :schema_validation_warnings, [GraphQL::STRING_TYPE], null: true,
+ description: 'Dashboard schema validation warnings'
+
field :annotations, Types::Metrics::Dashboards::AnnotationType.connection_type, null: true,
description: 'Annotations added to the dashboard',
resolver: Resolvers::Metrics::Dashboards::AnnotationResolver
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 33ba2af00e9..f87f97722fb 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -39,7 +39,8 @@ module Ci
dotenv: '.env',
cobertura: 'cobertura-coverage.xml',
terraform: 'tfplan.json',
- cluster_applications: 'gl-cluster-applications.json'
+ cluster_applications: 'gl-cluster-applications.json',
+ requirements: 'requirements.json'
}.freeze
INTERNAL_TYPES = {
@@ -71,7 +72,8 @@ module Ci
license_management: :raw,
license_scanning: :raw,
performance: :raw,
- terraform: :raw
+ terraform: :raw,
+ requirements: :raw
}.freeze
DOWNLOADABLE_TYPES = %w[
@@ -90,6 +92,7 @@ module Ci
metrics
performance
sast
+ requirements
].freeze
TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze
@@ -182,7 +185,8 @@ module Ci
terraform: 18, # Transformed json
accessibility: 19,
cluster_applications: 20,
- secret_detection: 21 ## EE-specific
+ secret_detection: 21, ## EE-specific
+ requirements: 22 ## EE-specific
}
enum file_format: {
diff --git a/app/models/performance_monitoring/prometheus_dashboard.rb b/app/models/performance_monitoring/prometheus_dashboard.rb
index f64ca3c66a1..b04e7e689cd 100644
--- a/app/models/performance_monitoring/prometheus_dashboard.rb
+++ b/app/models/performance_monitoring/prometheus_dashboard.rb
@@ -15,15 +15,17 @@ module PerformanceMonitoring
end
def find_for(project:, user:, path:, options: {})
- dashboard_response = Gitlab::Metrics::Dashboard::Finder.find(project, user, options.merge(dashboard_path: path))
- return unless dashboard_response[:status] == :success
-
- new(
- {
- path: path,
- environment: options[:environment]
- }.merge(dashboard_response[:dashboard])
- )
+ template = { path: path, environment: options[:environment] }
+ rsp = Gitlab::Metrics::Dashboard::Finder.find(project, user, options.merge(dashboard_path: path))
+
+ case rsp[:http_status] || rsp[:status]
+ when :success
+ new(template.merge(rsp[:dashboard] || {})) # when there is empty dashboard file returned rsp is still a success
+ when :unprocessable_entity
+ new(template) # validation error
+ else
+ nil # any other error
+ end
end
private
@@ -42,6 +44,15 @@ module PerformanceMonitoring
self.as_json(only: yaml_valid_attributes).to_yaml
end
+ # This method is planned to be refactored as a part of https://gitlab.com/gitlab-org/gitlab/-/issues/219398
+ # implementation. For new existing logic was reused to faster deliver MVC
+ def schema_validation_warnings
+ self.class.from_json(self.as_json)
+ nil
+ rescue ActiveModel::ValidationError => exception
+ exception.model.errors.map { |attr, error| "#{attr}: #{error}" }
+ end
+
private
def yaml_valid_attributes
diff --git a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml
index ad0d51d28f9..8ea75087fed 100644
--- a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml
+++ b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml
@@ -3,7 +3,7 @@
- if defined?(@breadcrumb_dropdown_links) && @breadcrumb_dropdown_links.key?(dropdown_location)
%li.dropdown
%button.text-expander.has-tooltip.js-breadcrumbs-collapsed-expander{ type: "button", data: { toggle: "dropdown", container: "body" }, "aria-label": button_tooltip, title: button_tooltip }
- = icon("ellipsis-h")
+ = sprite_icon("ellipsis_h", size: 12)
= sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle")
.dropdown-menu
%ul
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index 744aef3cad4..8dd0e5a92a7 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -15,5 +15,5 @@
qa_selector: qa_selector }
- else
= text_area_tag attr, current_text, class: classes, placeholder: placeholder
- %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" }
- = icon('compress')
+ %a.zen-control.zen-control-leave.js-zen-leave.gl-text-gray-700{ href: "#" }
+ = sprite_icon('compress', size: 16)
diff --git a/changelogs/unreleased/mwaw-210289-add-metrics-dashboard-validation-to-grapql.yml b/changelogs/unreleased/mwaw-210289-add-metrics-dashboard-validation-to-grapql.yml
new file mode 100644
index 00000000000..675f08ffe27
--- /dev/null
+++ b/changelogs/unreleased/mwaw-210289-add-metrics-dashboard-validation-to-grapql.yml
@@ -0,0 +1,5 @@
+---
+title: Add dashboard schema validation warnings as metrics dashboard GraphQL field
+merge_request: 33592
+author:
+type: added
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 0fa165554d9..8764320959b 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -242,6 +242,8 @@
- 1
- - repository_update_remote_mirror
- 1
+- - requirements_management_process_requirements_reports
+ - 1
- - security_scans
- 2
- - self_monitoring_project_create
diff --git a/db/post_migrate/20200518114540_schedule_fix_ruby_object_in_audit_events.rb b/db/post_migrate/20200518114540_schedule_fix_ruby_object_in_audit_events.rb
new file mode 100644
index 00000000000..e4335089540
--- /dev/null
+++ b/db/post_migrate/20200518114540_schedule_fix_ruby_object_in_audit_events.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+class ScheduleFixRubyObjectInAuditEvents < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'index_audit_events_on_ruby_object_in_details'
+ INTERVAL = 2.minutes.to_i
+ BATCH_SIZE = 1_000
+ MIGRATION = 'FixRubyObjectInAuditEvents'
+
+ disable_ddl_transaction!
+
+ class AuditEvent < ActiveRecord::Base
+ self.table_name = 'audit_events'
+
+ include ::EachBatch
+ end
+
+ def up
+ return unless Gitlab.ee?
+
+ # create temporary index for audit_events with ruby/object in details field, may take well over 1h
+ add_concurrent_index(:audit_events, :id, where: "details ~~ '%ruby/object%'", name: INDEX_NAME)
+
+ relation = AuditEvent.where("details ~~ '%ruby/object%'")
+
+ queue_background_migration_jobs_by_range_at_intervals(
+ relation,
+ MIGRATION,
+ INTERVAL,
+ batch_size: BATCH_SIZE
+ )
+ end
+
+ def down
+ # temporary index is to be dropped in a different migration in an upcoming release
+ # https://gitlab.com/gitlab-org/gitlab/issues/196842
+ remove_concurrent_index_by_name(:audit_events, INDEX_NAME)
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 7acc76ab99b..85f8e278ccf 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -9229,6 +9229,8 @@ CREATE INDEX index_approvers_on_user_id ON public.approvers USING btree (user_id
CREATE INDEX index_audit_events_on_entity_id_and_entity_type_and_id_desc ON public.audit_events USING btree (entity_id, entity_type, id DESC);
+CREATE INDEX index_audit_events_on_ruby_object_in_details ON public.audit_events USING btree (id) WHERE (details ~~ '%ruby/object%'::text);
+
CREATE INDEX index_award_emoji_on_awardable_type_and_awardable_id ON public.award_emoji USING btree (awardable_type, awardable_id);
CREATE INDEX index_award_emoji_on_user_id_and_name ON public.award_emoji USING btree (user_id, name);
@@ -13764,6 +13766,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200514000340
20200515155620
20200518091745
+20200518114540
20200518133123
20200519074709
20200519101002
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index e70f28d4b02..9a70d71a202 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -1725,6 +1725,13 @@ type CreateSnippetPayload {
snippet: Snippet
}
+enum DastScanTypeEnum {
+ """
+ Passive DAST scan. This scan will not make active attacks against the target site.
+ """
+ PASSIVE
+}
+
"""
Autogenerated input type of DeleteAnnotation
"""
@@ -7040,6 +7047,11 @@ type MetricsDashboard {
Path to a file with the dashboard definition
"""
path: String
+
+ """
+ Dashboard schema validation warnings
+ """
+ schemaValidationWarnings: [String!]
}
type MetricsDashboardAnnotation {
@@ -7269,6 +7281,7 @@ type Mutation {
mergeRequestSetWip(input: MergeRequestSetWipInput!): MergeRequestSetWipPayload
removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload
removeProjectFromSecurityDashboard(input: RemoveProjectFromSecurityDashboardInput!): RemoveProjectFromSecurityDashboardPayload
+ runDastScan(input: RunDASTScanInput!): RunDASTScanPayload
todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload
todoRestore(input: TodoRestoreInput!): TodoRestorePayload
todoRestoreMany(input: TodoRestoreManyInput!): TodoRestoreManyPayload
@@ -10298,6 +10311,56 @@ type RootStorageStatistics {
}
"""
+Autogenerated input type of RunDASTScan
+"""
+input RunDASTScanInput {
+ """
+ The branch to be associated with the scan.
+ """
+ branch: String!
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The project the DAST scan belongs to.
+ """
+ projectPath: ID!
+
+ """
+ The type of scan to be run.
+ """
+ scanType: DastScanTypeEnum!
+
+ """
+ The URL of the target to be scanned.
+ """
+ targetUrl: String!
+}
+
+"""
+Autogenerated return type of RunDASTScan
+"""
+type RunDASTScanPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Errors encountered during execution of the mutation.
+ """
+ errors: [String!]!
+
+ """
+ URL of the pipeline that was created.
+ """
+ pipelineUrl: String
+}
+
+"""
A Sentry error.
"""
type SentryDetailedError {
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index ed2631363a0..7c31d5ab41c 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -4571,6 +4571,23 @@
"possibleTypes": null
},
{
+ "kind": "ENUM",
+ "name": "DastScanTypeEnum",
+ "description": null,
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "PASSIVE",
+ "description": "Passive DAST scan. This scan will not make active attacks against the target site.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
"kind": "INPUT_OBJECT",
"name": "DeleteAnnotationInput",
"description": "Autogenerated input type of DeleteAnnotation",
@@ -19616,6 +19633,28 @@
},
"isDeprecated": false,
"deprecationReason": null
+ },
+ {
+ "name": "schemaValidationWarnings",
+ "description": "Dashboard schema validation warnings",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
}
],
"inputFields": null,
@@ -21285,6 +21324,33 @@
"deprecationReason": null
},
{
+ "name": "runDastScan",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "RunDASTScanInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "RunDASTScanPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "todoMarkDone",
"description": null,
"args": [
@@ -30139,6 +30205,150 @@
"possibleTypes": null
},
{
+ "kind": "INPUT_OBJECT",
+ "name": "RunDASTScanInput",
+ "description": "Autogenerated input type of RunDASTScan",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "projectPath",
+ "description": "The project the DAST scan belongs to.",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "targetUrl",
+ "description": "The URL of the target to be scanned.",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "branch",
+ "description": "The branch to be associated with the scan.",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "scanType",
+ "description": "The type of scan to be run.",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "DastScanTypeEnum",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "RunDASTScanPayload",
+ "description": "Autogenerated return type of RunDASTScan",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Errors encountered during execution of the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pipelineUrl",
+ "description": "URL of the pipeline that was created.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
"kind": "OBJECT",
"name": "SentryDetailedError",
"description": "A Sentry error.",
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 42a907f8845..00429423bb4 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1060,6 +1060,7 @@ Autogenerated return type of MergeRequestSetWip
| Name | Type | Description |
| --- | ---- | ---------- |
| `path` | String | Path to a file with the dashboard definition |
+| `schemaValidationWarnings` | String! => Array | Dashboard schema validation warnings |
## MetricsDashboardAnnotation
@@ -1450,6 +1451,16 @@ Counts of requirements by their state.
| `storageSize` | Float! | The total storage in bytes |
| `wikiSize` | Float! | The wiki size in bytes |
+## RunDASTScanPayload
+
+Autogenerated return type of RunDASTScan
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+| `pipelineUrl` | String | URL of the pipeline that was created. |
+
## SentryDetailedError
A Sentry error.
diff --git a/doc/update/upgrading_from_source.md b/doc/update/upgrading_from_source.md
index 9f8d4901cde..031984143da 100644
--- a/doc/update/upgrading_from_source.md
+++ b/doc/update/upgrading_from_source.md
@@ -383,6 +383,18 @@ Example:
Additional instructions here.
-->
+### 13.0.1
+
+As part of [deprecating Rack Attack throttles on Omnibus GitLab](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/4750), Rack Attack initializer on GitLab
+was renamed from [`config/initializers/rack_attack_new.rb` to `config/initializers/rack_attack.rb`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33072).
+If this file exists on your installation, consider creating a backup before updating:
+
+```shell
+cd /home/git/gitlab
+
+cp config/initializers/rack_attack.rb config/initializers/rack_attack_backup.rb
+```
+
## Troubleshooting
### 1. Revert the code to the previous version
diff --git a/doc/user/application_security/threat_monitoring/index.md b/doc/user/application_security/threat_monitoring/index.md
index 6c04dd63dbe..434048896fe 100644
--- a/doc/user/application_security/threat_monitoring/index.md
+++ b/doc/user/application_security/threat_monitoring/index.md
@@ -9,9 +9,10 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14707) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.9.
-The **Threat Monitoring** page provides metrics for the GitLab
-application runtime security features. You can access these metrics by
-navigating to your project's **Security & Compliance > Threat Monitoring** page.
+The **Threat Monitoring** page provides metrics and policy management
+for the GitLab application runtime security features. You can access
+these by navigating to your project's **Security & Compliance > Threat
+Monitoring** page.
GitLab supports statistics for the following security features:
@@ -77,3 +78,41 @@ about your packet flow:
If a significant percentage of packets is dropped, you should
investigate it for potential threats by
[examining the Cilium logs](../../clusters/applications.md#install-cilium-using-gitlab-cicd).
+
+## Container Network Policy management
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3328) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.1.
+
+The **Threat Monitoring** page's **Policy** tab displays deployed
+network policies for all available environments. You can check a
+network policy's `yaml` manifest and toggle the policy's enforcement
+status. This section has the following prerequisites:
+
+- Your project contains at least one [environment](../../../ci/environments/index.md)
+- You've [installed Cilium](../../clusters/applications.md#install-cilium-using-gitlab-cicd)
+
+Network policies are fetched directly from the selected environment's
+deployment platform. Changes performed outside of this tab are
+reflected upon refresh. Enforcement status changes are deployed
+directly to a deployment namespace of the selected environment.
+
+NOTE: **Note:**
+If you're using [Auto DevOps](../../../topics/autodevops/index.md) and
+change a policy in this section, your `auto-deploy-values.yaml` file
+doesn't update. Auto DevOps users must make changes by following
+the [Container Network Policy documentation](../../../topics/autodevops/stages.md#network-policy).
+
+### Changing enforcement status
+
+To change a network policy's enforcement status:
+
+- Click the network policy you want to update.
+- Click the **Enforcement status** toggle to update the selected policy.
+- Click the **Apply changes** button to deploy network policy changes.
+
+NOTE: **Note:**
+Disabled network policies have the
+`network-policy.gitlab.com/disabled_by: gitlab` selector inside the
+`podSelector` block. This narrows the scope of such a policy and as a
+result it doesn't affect any pods. The policy itself is still deployed
+to the corresponding deployment namespace.
diff --git a/doc/user/project/code_owners.md b/doc/user/project/code_owners.md
index 81bdf1dfb2f..9b0512830c7 100644
--- a/doc/user/project/code_owners.md
+++ b/doc/user/project/code_owners.md
@@ -80,6 +80,14 @@ Once set, Code Owners are displayed in merge requests widgets:
NOTE: **Note**:
While the`CODEOWNERS` file can be used in addition to Merge Request [Approval Rules](merge_requests/merge_request_approvals.md#approval-rules) it can also be used as the sole driver of a Merge Request approval (without using [Approval Rules](merge_requests/merge_request_approvals.md#approval-rules)) by simply creating the file in one of the three locations specified above, configuring the Code Owners to be required approvers for [protected branches](protected_branches.md#protected-branches-approval-by-code-owners-premium) and then using [the syntax of Code Owners files](code_owners.md#the-syntax-of-code-owners-files) to specify the actual owners and granular permissions.
+NOTE: **Note**:
+ Using Code Owners in conjunction with [Protected Branches Approvals](protected_branches.md#protected-branches-approval-by-code-owners-premium)
+ will prevent any user who is not specified in the `CODEOWNERS` file from pushing changes
+for the specified files/paths, even if their role is included in the **Allowed to push** column.
+This allows for a more inclusive push strategy, as administrators don't have to restrict developers
+ from pushing directly to the protected branch, but can restrict pushing to certain
+files where a review by Code Owners is required.
+
## The syntax of Code Owners files
Files can be specified using the same kind of patterns you would use
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index 2eae2bc9915..215894e137b 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -362,6 +362,8 @@ When **Metrics Dashboard YAML definition is invalid** at least one of the follow
1. `query_range: can't be blank` [learn more](#metrics-metrics-properties)
1. `unit: can't be blank` [learn more](#metrics-metrics-properties)
+Metrics Dashboard YAML definition validation information is also available as a [GraphQL API field](../../../api/graphql/reference/index.md#metricsdashboard)
+
#### Dashboard YAML properties
Dashboards have several components:
diff --git a/lib/gitlab/background_migration/fix_ruby_object_in_audit_events.rb b/lib/gitlab/background_migration/fix_ruby_object_in_audit_events.rb
new file mode 100644
index 00000000000..46921a070c3
--- /dev/null
+++ b/lib/gitlab/background_migration/fix_ruby_object_in_audit_events.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Remove serialized Ruby object in audit_events
+ class FixRubyObjectInAuditEvents
+ def perform(start_id, stop_id)
+ end
+ end
+ end
+end
+
+Gitlab::BackgroundMigration::FixRubyObjectInAuditEvents.prepend_if_ee('EE::Gitlab::BackgroundMigration::FixRubyObjectInAuditEvents')
diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb
index 20bd2856b07..74736b24d73 100644
--- a/lib/gitlab/ci/config/entry/reports.rb
+++ b/lib/gitlab/ci/config/entry/reports.rb
@@ -14,7 +14,8 @@ module Gitlab
ALLOWED_KEYS =
%i[junit codequality sast secret_detection dependency_scanning container_scanning
dast performance license_management license_scanning metrics lsif
- dotenv cobertura terraform accessibility cluster_applications].freeze
+ dotenv cobertura terraform accessibility cluster_applications
+ requirements].freeze
attributes ALLOWED_KEYS
@@ -40,6 +41,7 @@ module Gitlab
validates :terraform, array_of_strings_or_string: true
validates :accessibility, array_of_strings_or_string: true
validates :cluster_applications, array_of_strings_or_string: true
+ validates :requirements, array_of_strings_or_string: true
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 0d65df72a02..11b4be0f766 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3091,9 +3091,6 @@ msgstr ""
msgid "Auto DevOps, runners and job artifacts"
msgstr ""
-msgid "Auto License Compliance"
-msgstr ""
-
msgid "Auto stop successfully canceled."
msgstr ""
@@ -8417,9 +8414,6 @@ msgstr ""
msgid "Enter your password to approve"
msgstr ""
-msgid "Enter zen mode"
-msgstr ""
-
msgid "Environment"
msgstr ""
@@ -10637,9 +10631,6 @@ msgstr ""
msgid "Gitea Import"
msgstr ""
-msgid "Gitlab CI/CD"
-msgstr ""
-
msgid "Gitlab Pages"
msgstr ""
@@ -11700,6 +11691,9 @@ msgstr ""
msgid "Import all compatible projects"
msgstr ""
+msgid "Import all compatible repositories"
+msgstr ""
+
msgid "Import all projects"
msgstr ""
@@ -11889,6 +11883,9 @@ msgstr ""
msgid "Incompatible options set!"
msgstr ""
+msgid "Incompatible project"
+msgstr ""
+
msgid "Indent"
msgstr ""
@@ -12886,6 +12883,9 @@ msgstr ""
msgid "Leave the \"File type\" and \"Delivery method\" options on their default values."
msgstr ""
+msgid "Leave zen mode"
+msgstr ""
+
msgid "Let's Encrypt does not accept emails on example.com"
msgstr ""
@@ -12925,15 +12925,9 @@ msgstr ""
msgid "LicenseCompliance|Deny"
msgstr ""
-msgid "LicenseCompliance|Here you can allow or deny licenses for this project. Using %{ci} or %{license} will allow you to see if there are any unmanaged licenses and allow or deny them in merge request."
-msgstr ""
-
msgid "LicenseCompliance|License"
msgstr ""
-msgid "LicenseCompliance|License Compliance"
-msgstr ""
-
msgid "LicenseCompliance|License Compliance detected %d license and policy violation for the source branch only; approval required"
msgid_plural "LicenseCompliance|License Compliance detected %d licenses and policy violations for the source branch only; approval required"
msgstr[0] ""
diff --git a/package.json b/package.json
index 8e60064f29f..44abf908abd 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,7 @@
"@babel/preset-env": "^7.8.4",
"@gitlab/at.js": "1.5.5",
"@gitlab/svgs": "1.137.0",
- "@gitlab/ui": "16.2.1",
+ "@gitlab/ui": "16.3.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-1",
"@sentry/browser": "^5.10.2",
diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb
index aa27c030b78..a7a0f6f57b6 100644
--- a/qa/qa/page/project/settings/ci_cd.rb
+++ b/qa/qa/page/project/settings/ci_cd.rb
@@ -42,5 +42,3 @@ module QA
end
end
end
-
-QA::Page::Project::Settings::CICD.prepend_if_ee('QA::EE::Page::Project::Settings::CICD')
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 9491b52c888..978f6c3ea41 100644
--- a/spec/frontend/import_projects/components/import_projects_table_spec.js
+++ b/spec/frontend/import_projects/components/import_projects_table_spec.js
@@ -1,11 +1,21 @@
+import { nextTick } from 'vue';
import Vuex from 'vuex';
-import { createLocalVue, mount } from '@vue/test-utils';
-import { state, actions, getters, mutations } from '~/import_projects/store';
-import importProjectsTable from '~/import_projects/components/import_projects_table.vue';
-import STATUS_MAP from '~/import_projects/constants';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon, GlButton } from '@gitlab/ui';
+import { state, getters } from '~/import_projects/store';
+import eventHub from '~/import_projects/event_hub';
+import ImportProjectsTable from '~/import_projects/components/import_projects_table.vue';
+import ImportedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
+import ProviderRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
+import IncompatibleRepoTableRow from '~/import_projects/components/incompatible_repo_table_row.vue';
+
+jest.mock('~/import_projects/event_hub', () => ({
+ $emit: jest.fn(),
+}));
describe('ImportProjectsTable', () => {
- let vm;
+ let wrapper;
+
const providerTitle = 'THE PROVIDER';
const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' };
const importedProject = {
@@ -16,176 +26,164 @@ describe('ImportProjectsTable', () => {
importSource: 'importSource',
};
- function initStore() {
- const stubbedActions = {
- ...actions,
- fetchJobs: jest.fn(),
- fetchRepos: jest.fn(actions.requestRepos),
- fetchImport: jest.fn(actions.requestImport),
- };
-
- const store = new Vuex.Store({
- state: state(),
- actions: stubbedActions,
- mutations,
- getters,
- });
-
- return store;
- }
+ const findImportAllButton = () =>
+ wrapper
+ .findAll(GlButton)
+ .filter(w => w.props().variant === 'success')
+ .at(0);
- function mountComponent() {
+ function createComponent({ state: initialState, getters: customGetters, slots } = {}) {
const localVue = createLocalVue();
localVue.use(Vuex);
- const store = initStore();
+ const store = new Vuex.Store({
+ state: { ...state(), ...initialState },
+ getters: {
+ ...getters,
+ ...customGetters,
+ },
+ actions: {
+ fetchRepos: jest.fn(),
+ fetchReposFiltered: jest.fn(),
+ fetchJobs: jest.fn(),
+ stopJobsPolling: jest.fn(),
+ clearJobsEtagPoll: jest.fn(),
+ setFilter: jest.fn(),
+ },
+ });
- const component = mount(importProjectsTable, {
+ wrapper = shallowMount(ImportProjectsTable, {
localVue,
store,
propsData: {
providerTitle,
},
+ slots,
});
-
- return component.vm;
}
- beforeEach(() => {
- vm = mountComponent();
- });
-
afterEach(() => {
- vm.$destroy();
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
});
- it('renders a loading icon while repos are loading', () =>
- vm.$nextTick().then(() => {
- expect(vm.$el.querySelector('.js-loading-button-icon')).not.toBeNull();
- }));
-
- it('renders a table with imported projects and provider repos', () => {
- vm.$store.dispatch('receiveReposSuccess', {
- importedProjects: [importedProject],
- providerRepos: [providerRepo],
- namespaces: [{ path: 'path' }],
+ it('renders a loading icon while repos are loading', () => {
+ createComponent({
+ state: {
+ isLoadingRepos: true,
+ },
});
- return vm.$nextTick().then(() => {
- expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
- expect(vm.$el.querySelector('.table')).not.toBeNull();
- expect(vm.$el.querySelector('.import-jobs-from-col').innerText).toMatch(
- `From ${providerTitle}`,
- );
-
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
- });
+ expect(wrapper.contains(GlLoadingIcon)).toBe(true);
});
- it('renders an empty state if there are no imported projects or provider repos', () => {
- vm.$store.dispatch('receiveReposSuccess', {
- importedProjects: [],
- providerRepos: [],
- namespaces: [],
+ it('renders a table with imported projects and provider repos', () => {
+ createComponent({
+ state: {
+ importedProjects: [importedProject],
+ providerRepos: [providerRepo],
+ incompatibleRepos: [{ ...providerRepo, id: 11 }],
+ namespaces: [{ path: 'path' }],
+ },
});
- 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 found`);
- });
+ expect(wrapper.contains(GlLoadingIcon)).toBe(false);
+ expect(wrapper.contains('table')).toBe(true);
+ expect(
+ wrapper
+ .findAll('th')
+ .filter(w => w.text() === `From ${providerTitle}`)
+ .isEmpty(),
+ ).toBe(false);
+
+ expect(wrapper.contains(ProviderRepoTableRow)).toBe(true);
+ expect(wrapper.contains(ImportedProjectTableRow)).toBe(true);
+ expect(wrapper.contains(IncompatibleRepoTableRow)).toBe(true);
});
- it('shows loading spinner when bulk import button is clicked', () => {
- vm.$store.dispatch('receiveReposSuccess', {
- importedProjects: [],
- providerRepos: [providerRepo],
- namespaces: [{ path: 'path' }],
- });
-
- return vm
- .$nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.js-imported-project')).toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
-
- vm.$el.querySelector('.js-import-all').click();
- })
- .then(() => vm.$nextTick())
- .then(() => {
- expect(vm.$el.querySelector('.js-import-all .js-loading-button-icon')).not.toBeNull();
+ it.each`
+ hasIncompatibleRepos | buttonText
+ ${false} | ${'Import all repositories'}
+ ${true} | ${'Import all compatible repositories'}
+ `(
+ 'import all button has "$buttonText" text when hasIncompatibleRepos is $hasIncompatibleRepos',
+ ({ hasIncompatibleRepos, buttonText }) => {
+ createComponent({
+ state: {
+ providerRepos: [providerRepo],
+ },
+ getters: {
+ hasIncompatibleRepos: () => hasIncompatibleRepos,
+ },
});
- });
- it('imports provider repos if bulk import button is clicked', () => {
- mountComponent();
+ expect(findImportAllButton().text()).toBe(buttonText);
+ },
+ );
- vm.$store.dispatch('receiveReposSuccess', {
- importedProjects: [],
- providerRepos: [providerRepo],
- namespaces: [{ path: 'path' }],
+ it('renders an empty state if there are no projects available', () => {
+ createComponent({
+ state: {
+ importedProjects: [],
+ providerRepos: [],
+ incompatibleProjects: [],
+ },
});
- return vm
- .$nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.js-imported-project')).toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
-
- vm.$store.dispatch('receiveImportSuccess', { importedProject, repoId: providerRepo.id });
- })
- .then(() => vm.$nextTick())
- .then(() => {
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).toBeNull();
- });
+ expect(wrapper.contains(ProviderRepoTableRow)).toBe(false);
+ expect(wrapper.contains(ImportedProjectTableRow)).toBe(false);
+ expect(wrapper.text()).toContain(`No ${providerTitle} repositories found`);
});
- it('polls to update the status of imported projects', () => {
- const updatedProjects = [
- {
- id: importedProject.id,
- importStatus: 'finished',
+ it('sends importAll event when import button is clicked', async () => {
+ createComponent({
+ state: {
+ providerRepos: [providerRepo],
},
- ];
-
- vm.$store.dispatch('receiveReposSuccess', {
- importedProjects: [importedProject],
- providerRepos: [],
- namespaces: [{ path: 'path' }],
});
- return vm
- .$nextTick()
- .then(() => {
- const statusObject = STATUS_MAP[importedProject.importStatus];
-
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
- statusObject.text,
- );
-
- expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
-
- vm.$store.dispatch('receiveJobsSuccess', updatedProjects);
- })
- .then(() => vm.$nextTick())
- .then(() => {
- const statusObject = STATUS_MAP[updatedProjects[0].importStatus];
+ findImportAllButton().vm.$emit('click');
+ await nextTick();
+ expect(eventHub.$emit).toHaveBeenCalledWith('importAll');
+ });
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
- statusObject.text,
- );
+ it('shows loading spinner when import is in progress', () => {
+ createComponent({
+ getters: {
+ isImportingAnyRepo: () => true,
+ },
+ });
- expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
- });
+ expect(findImportAllButton().props().loading).toBe(true);
});
it('renders filtering input field', () => {
- expect(
- vm.$el.querySelector('input[data-qa-selector="githubish_import_filter_field"]'),
- ).not.toBeNull();
+ createComponent();
+ expect(wrapper.contains('input[data-qa-selector="githubish_import_filter_field"]')).toBe(true);
});
+
+ it.each`
+ hasIncompatibleRepos | shouldRenderSlot | action
+ ${false} | ${false} | ${'does not render'}
+ ${true} | ${true} | ${'render'}
+ `(
+ '$action incompatible-repos-warning slot if hasIncompatibleRepos is $hasIncompatibleRepos',
+ ({ hasIncompatibleRepos, shouldRenderSlot }) => {
+ const INCOMPATIBLE_TEXT = 'INCOMPATIBLE!';
+
+ createComponent({
+ getters: {
+ hasIncompatibleRepos: () => hasIncompatibleRepos,
+ },
+
+ slots: {
+ 'incompatible-repos-warning': INCOMPATIBLE_TEXT,
+ },
+ });
+
+ expect(wrapper.text().includes(INCOMPATIBLE_TEXT)).toBe(shouldRenderSlot);
+ },
+ );
});
diff --git a/spec/frontend/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
index 8be645c496f..f5e5141eac8 100644
--- a/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
@@ -6,7 +6,7 @@ import STATUS_MAP, { STATUSES } from '~/import_projects/constants';
describe('ProviderRepoTableRow', () => {
let vm;
- const fetchImport = jest.fn((context, data) => actions.requestImport(context, data));
+ const fetchImport = jest.fn();
const importPath = '/import-path';
const defaultTargetNamespace = 'user';
const ciCdOnly = true;
@@ -17,11 +17,11 @@ describe('ProviderRepoTableRow', () => {
providerLink: 'providerLink',
};
- function initStore() {
+ function initStore(initialState) {
const stubbedActions = { ...actions, fetchImport };
const store = new Vuex.Store({
- state: state(),
+ state: { ...state(), ...initialState },
actions: stubbedActions,
mutations,
getters,
@@ -30,12 +30,11 @@ describe('ProviderRepoTableRow', () => {
return store;
}
- function mountComponent() {
+ function mountComponent(initialState) {
const localVue = createLocalVue();
localVue.use(Vuex);
- const store = initStore();
- store.dispatch('setInitialData', { importPath, defaultTargetNamespace, ciCdOnly });
+ const store = initStore({ importPath, defaultTargetNamespace, ciCdOnly, ...initialState });
const component = mount(providerRepoTableRow, {
localVue,
diff --git a/spec/frontend/import_projects/store/actions_spec.js b/spec/frontend/import_projects/store/actions_spec.js
index 4954513715e..1f2882a2532 100644
--- a/spec/frontend/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_projects/store/actions_spec.js
@@ -4,7 +4,6 @@ import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import {
- SET_INITIAL_DATA,
REQUEST_REPOS,
RECEIVE_REPOS_SUCCESS,
RECEIVE_REPOS_ERROR,
@@ -14,14 +13,7 @@ import {
RECEIVE_JOBS_SUCCESS,
} from '~/import_projects/store/mutation_types';
import {
- setInitialData,
- requestRepos,
- receiveReposSuccess,
- receiveReposError,
fetchRepos,
- requestImport,
- receiveImportSuccess,
- receiveImportError,
fetchImport,
receiveJobsSuccess,
fetchJobs,
@@ -32,7 +24,6 @@ import state from '~/import_projects/store/state';
describe('import_projects store actions', () => {
let localState;
- const repoId = 1;
const repos = [{ id: 1 }, { id: 2 }];
const importPayload = { newName: 'newName', targetNamespace: 'targetNamespace', repo: { id: 1 } };
@@ -40,61 +31,6 @@ describe('import_projects store actions', () => {
localState = state();
});
- describe('setInitialData', () => {
- it(`commits ${SET_INITIAL_DATA} mutation`, done => {
- const initialData = {
- reposPath: 'reposPath',
- provider: 'provider',
- jobsPath: 'jobsPath',
- importPath: 'impapp/assets/javascripts/vue_shared/components/select2_select.vueortPath',
- defaultTargetNamespace: 'defaultTargetNamespace',
- ciCdOnly: 'ciCdOnly',
- canSelectNamespace: 'canSelectNamespace',
- };
-
- testAction(
- setInitialData,
- initialData,
- localState,
- [{ type: SET_INITIAL_DATA, payload: initialData }],
- [],
- done,
- );
- });
- });
-
- describe('requestRepos', () => {
- it(`requestRepos commits ${REQUEST_REPOS} mutation`, done => {
- testAction(
- requestRepos,
- null,
- localState,
- [{ type: REQUEST_REPOS, payload: null }],
- [],
- done,
- );
- });
- });
-
- describe('receiveReposSuccess', () => {
- it(`commits ${RECEIVE_REPOS_SUCCESS} mutation`, done => {
- testAction(
- receiveReposSuccess,
- repos,
- localState,
- [{ type: RECEIVE_REPOS_SUCCESS, payload: repos }],
- [],
- done,
- );
- });
- });
-
- describe('receiveReposError', () => {
- it(`commits ${RECEIVE_REPOS_ERROR} mutation`, done => {
- testAction(receiveReposError, repos, localState, [{ type: RECEIVE_REPOS_ERROR }], [], done);
- });
- });
-
describe('fetchRepos', () => {
let mock;
const payload = { imported_projects: [{}], provider_repos: [{}], namespaces: [{}] };
@@ -106,39 +42,33 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore());
- it('dispatches stopJobsPolling, requestRepos and receiveReposSuccess actions on a successful request', done => {
+ it('dispatches stopJobsPolling actions and commits REQUEST_REPOS, RECEIVE_REPOS_SUCCESS mutations on a successful request', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, payload);
- testAction(
+ return testAction(
fetchRepos,
null,
localState,
- [],
[
- { type: 'stopJobsPolling' },
- { type: 'requestRepos' },
+ { type: REQUEST_REPOS },
{
- type: 'receiveReposSuccess',
+ type: RECEIVE_REPOS_SUCCESS,
payload: convertObjectPropsToCamelCase(payload, { deep: true }),
},
- {
- type: 'fetchJobs',
- },
],
- done,
+ [{ type: 'stopJobsPolling' }, { type: 'fetchJobs' }],
);
});
- it('dispatches stopJobsPolling, requestRepos and receiveReposError actions on an unsuccessful request', done => {
+ it('dispatches stopJobsPolling action and commits REQUEST_REPOS, RECEIVE_REPOS_ERROR mutations on an unsuccessful request', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
- testAction(
+ return testAction(
fetchRepos,
null,
localState,
- [],
- [{ type: 'stopJobsPolling' }, { type: 'requestRepos' }, { type: 'receiveReposError' }],
- done,
+ [{ type: REQUEST_REPOS }, { type: RECEIVE_REPOS_ERROR }],
+ [{ type: 'stopJobsPolling' }],
);
});
@@ -147,72 +77,26 @@ describe('import_projects store actions', () => {
localState.filter = 'filter';
});
- it('fetches repos with filter applied', done => {
+ it('fetches repos with filter applied', () => {
mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(200, payload);
- testAction(
+ return testAction(
fetchRepos,
null,
localState,
- [],
[
- { type: 'stopJobsPolling' },
- { type: 'requestRepos' },
+ { type: REQUEST_REPOS },
{
- type: 'receiveReposSuccess',
+ type: RECEIVE_REPOS_SUCCESS,
payload: convertObjectPropsToCamelCase(payload, { deep: true }),
},
- {
- type: 'fetchJobs',
- },
],
- done,
+ [{ type: 'stopJobsPolling' }, { type: 'fetchJobs' }],
);
});
});
});
- describe('requestImport', () => {
- it(`commits ${REQUEST_IMPORT} mutation`, done => {
- testAction(
- requestImport,
- repoId,
- localState,
- [{ type: REQUEST_IMPORT, payload: repoId }],
- [],
- done,
- );
- });
- });
-
- describe('receiveImportSuccess', () => {
- it(`commits ${RECEIVE_IMPORT_SUCCESS} mutation`, done => {
- const payload = { importedProject: { name: 'imported/project' }, repoId: 2 };
-
- testAction(
- receiveImportSuccess,
- payload,
- localState,
- [{ type: RECEIVE_IMPORT_SUCCESS, payload }],
- [],
- done,
- );
- });
- });
-
- describe('receiveImportError', () => {
- it(`commits ${RECEIVE_IMPORT_ERROR} mutation`, done => {
- testAction(
- receiveImportError,
- repoId,
- localState,
- [{ type: RECEIVE_IMPORT_ERROR, payload: repoId }],
- [],
- done,
- );
- });
- });
-
describe('fetchImport', () => {
let mock;
@@ -223,56 +107,53 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore());
- it('dispatches requestImport and receiveImportSuccess actions on a successful request', done => {
+ it('commits REQUEST_IMPORT and REQUEST_IMPORT_SUCCESS mutations on a successful request', () => {
const importedProject = { name: 'imported/project' };
const importRepoId = importPayload.repo.id;
mock.onPost(`${TEST_HOST}/endpoint.json`).reply(200, importedProject);
- testAction(
+ return testAction(
fetchImport,
importPayload,
localState,
- [],
[
- { type: 'requestImport', payload: importRepoId },
+ { type: REQUEST_IMPORT, payload: importRepoId },
{
- type: 'receiveImportSuccess',
+ type: RECEIVE_IMPORT_SUCCESS,
payload: {
importedProject: convertObjectPropsToCamelCase(importedProject, { deep: true }),
repoId: importRepoId,
},
},
],
- done,
+ [],
);
});
- it('dispatches requestImport and receiveImportSuccess actions on an unsuccessful request', done => {
+ it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR on an unsuccessful request', () => {
mock.onPost(`${TEST_HOST}/endpoint.json`).reply(500);
- testAction(
+ return testAction(
fetchImport,
importPayload,
localState,
- [],
[
- { type: 'requestImport', payload: importPayload.repo.id },
- { type: 'receiveImportError', payload: { repoId: importPayload.repo.id } },
+ { type: REQUEST_IMPORT, payload: importPayload.repo.id },
+ { type: RECEIVE_IMPORT_ERROR, payload: importPayload.repo.id },
],
- done,
+ [],
);
});
});
describe('receiveJobsSuccess', () => {
- it(`commits ${RECEIVE_JOBS_SUCCESS} mutation`, done => {
- testAction(
+ it(`commits ${RECEIVE_JOBS_SUCCESS} mutation`, () => {
+ return testAction(
receiveJobsSuccess,
repos,
localState,
[{ type: RECEIVE_JOBS_SUCCESS, payload: repos }],
[],
- done,
);
});
});
@@ -293,21 +174,20 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore());
- it('dispatches requestJobs and receiveJobsSuccess actions on a successful request', done => {
+ it('commits RECEIVE_JOBS_SUCCESS mutation on a successful request', async () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, updatedProjects);
- testAction(
+ await testAction(
fetchJobs,
null,
localState,
- [],
[
{
- type: 'receiveJobsSuccess',
+ type: RECEIVE_JOBS_SUCCESS,
payload: convertObjectPropsToCamelCase(updatedProjects, { deep: true }),
},
],
- done,
+ [],
);
});
@@ -316,21 +196,20 @@ describe('import_projects store actions', () => {
localState.filter = 'filter';
});
- it('fetches realtime changes with filter applied', done => {
+ it('fetches realtime changes with filter applied', () => {
mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(200, updatedProjects);
- testAction(
+ return testAction(
fetchJobs,
null,
localState,
- [],
[
{
- type: 'receiveJobsSuccess',
+ type: RECEIVE_JOBS_SUCCESS,
payload: convertObjectPropsToCamelCase(updatedProjects, { deep: true }),
},
],
- done,
+ [],
);
});
});
diff --git a/spec/frontend/import_projects/store/getters_spec.js b/spec/frontend/import_projects/store/getters_spec.js
index e5e4a95f473..93d1ed89783 100644
--- a/spec/frontend/import_projects/store/getters_spec.js
+++ b/spec/frontend/import_projects/store/getters_spec.js
@@ -2,6 +2,7 @@ import {
namespaceSelectOptions,
isImportingAnyRepo,
hasProviderRepos,
+ hasIncompatibleRepos,
hasImportedProjects,
} from '~/import_projects/store/getters';
import state from '~/import_projects/store/state';
@@ -80,4 +81,18 @@ describe('import_projects store getters', () => {
expect(hasImportedProjects(localState)).toBe(false);
});
});
+
+ describe('hasIncompatibleRepos', () => {
+ it('returns true if there are any incompatibleProjects', () => {
+ localState.incompatibleRepos = new Array(1);
+
+ expect(hasIncompatibleRepos(localState)).toBe(true);
+ });
+
+ it('returns false if there are no incompatibleProjects', () => {
+ localState.incompatibleRepos = [];
+
+ expect(hasIncompatibleRepos(localState)).toBe(false);
+ });
+ });
});
diff --git a/spec/javascripts/vue_shared/components/deprecated_modal_2_spec.js b/spec/frontend/vue_shared/components/deprecated_modal_2_spec.js
index e031583b43a..b201a9acdd4 100644
--- a/spec/javascripts/vue_shared/components/deprecated_modal_2_spec.js
+++ b/spec/frontend/vue_shared/components/deprecated_modal_2_spec.js
@@ -1,6 +1,5 @@
-import $ from 'jquery';
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
const modalComponent = Vue.extend(DeprecatedModal2);
@@ -86,7 +85,7 @@ describe('DeprecatedModal2', () => {
});
});
- it('works with data-toggle="modal"', done => {
+ it('works with data-toggle="modal"', () => {
setFixtures(`
<button id="modal-button" data-toggle="modal" data-target="#my-modal"></button>
<div id="modal-container"></div>
@@ -101,9 +100,16 @@ describe('DeprecatedModal2', () => {
},
modalContainer,
);
- $(vm.$el).on('shown.bs.modal', () => done());
+ const modalElement = document.getElementById('my-modal');
modalButton.click();
+
+ expect(modalElement).not.toHaveClass('show');
+
+ // let the modal fade in
+ jest.runOnlyPendingTimers();
+
+ expect(modalElement).toHaveClass('show');
});
describe('methods', () => {
@@ -111,7 +117,7 @@ describe('DeprecatedModal2', () => {
beforeEach(() => {
vm = mountComponent(modalComponent, {});
- spyOn(vm, '$emit');
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
});
describe('emitCancel', () => {
@@ -149,23 +155,14 @@ describe('DeprecatedModal2', () => {
describe('slots', () => {
const slotContent = 'this should go into the slot';
- const modalWithSlot = slotName => {
- let template;
- if (slotName) {
- template = `
- <deprecated-modal-2>
- <template slot="${slotName}">${slotContent}</template>
- </deprecated-modal-2>
- `;
- } else {
- template = `<deprecated-modal-2>${slotContent}</deprecated-modal-2>`;
- }
+ const modalWithSlot = slot => {
return Vue.extend({
components: {
DeprecatedModal2,
},
- template,
+ render: h =>
+ h('deprecated-modal-2', [slot ? h('template', { slot }, slotContent) : slotContent]),
});
};
diff --git a/spec/javascripts/vue_shared/components/deprecated_modal_spec.js b/spec/frontend/vue_shared/components/deprecated_modal_spec.js
index d6c10e32794..b9793ce2d80 100644
--- a/spec/javascripts/vue_shared/components/deprecated_modal_spec.js
+++ b/spec/frontend/vue_shared/components/deprecated_modal_spec.js
@@ -1,6 +1,5 @@
-import $ from 'jquery';
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
const modalComponent = Vue.extend(DeprecatedModal);
@@ -47,7 +46,7 @@ describe('DeprecatedModal', () => {
});
});
- it('works with data-toggle="modal"', done => {
+ it('works with data-toggle="modal"', () => {
setFixtures(`
<button id="modal-button" data-toggle="modal" data-target="#my-modal"></button>
<div id="modal-container"></div>
@@ -63,9 +62,12 @@ describe('DeprecatedModal', () => {
modalContainer,
);
const modalElement = vm.$el.querySelector('#my-modal');
- $(modalElement).on('shown.bs.modal', () => done());
+
+ expect(modalElement).not.toHaveClass('show');
modalButton.click();
+
+ expect(modalElement).toHaveClass('show');
});
});
});
diff --git a/spec/frontend/vue_shared/directives/tooltip_spec.js b/spec/frontend/vue_shared/directives/tooltip_spec.js
new file mode 100644
index 00000000000..9d3dd3c5f75
--- /dev/null
+++ b/spec/frontend/vue_shared/directives/tooltip_spec.js
@@ -0,0 +1,98 @@
+import $ from 'jquery';
+import { mount } from '@vue/test-utils';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+describe('Tooltip directive', () => {
+ let vm;
+
+ afterEach(() => {
+ if (vm) {
+ vm.$destroy();
+ }
+ });
+
+ describe('with a single tooltip', () => {
+ beforeEach(() => {
+ const wrapper = mount(
+ {
+ directives: {
+ tooltip,
+ },
+ data() {
+ return {
+ tooltip: 'some text',
+ };
+ },
+ template: '<div v-tooltip :title="tooltip"></div>',
+ },
+ { attachToDocument: true },
+ );
+
+ vm = wrapper.vm;
+ });
+
+ it('should have tooltip plugin applied', () => {
+ expect($(vm.$el).data('bs.tooltip')).toBeDefined();
+ });
+
+ it('displays the title as tooltip', () => {
+ $(vm.$el).tooltip('show');
+ jest.runOnlyPendingTimers();
+
+ const tooltipElement = document.querySelector('.tooltip-inner');
+
+ expect(tooltipElement.textContent).toContain('some text');
+ });
+
+ it('updates a visible tooltip', () => {
+ $(vm.$el).tooltip('show');
+ jest.runOnlyPendingTimers();
+
+ const tooltipElement = document.querySelector('.tooltip-inner');
+
+ vm.tooltip = 'other text';
+
+ jest.runOnlyPendingTimers();
+
+ return vm.$nextTick().then(() => {
+ expect(tooltipElement.textContent).toContain('other text');
+ });
+ });
+ });
+
+ describe('with multiple tooltips', () => {
+ beforeEach(() => {
+ const wrapper = mount(
+ {
+ directives: {
+ tooltip,
+ },
+ template: `
+ <div>
+ <div
+ v-tooltip
+ class="js-look-for-tooltip"
+ title="foo">
+ </div>
+ <div
+ v-tooltip
+ title="bar">
+ </div>
+ </div>
+ `,
+ },
+ { attachToDocument: true },
+ );
+
+ vm = wrapper.vm;
+ });
+
+ it('should have tooltip plugin applied to all instances', () => {
+ expect(
+ $(vm.$el)
+ .find('.js-look-for-tooltip')
+ .data('bs.tooltip'),
+ ).toBeDefined();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/translate_spec.js b/spec/frontend/vue_shared/translate_spec.js
new file mode 100644
index 00000000000..42aa28a6309
--- /dev/null
+++ b/spec/frontend/vue_shared/translate_spec.js
@@ -0,0 +1,214 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import locale from '~/locale';
+import Translate from '~/vue_shared/translate';
+
+const localVue = createLocalVue();
+localVue.use(Translate);
+
+describe('Vue translate filter', () => {
+ const createTranslationMock = (key, ...translations) => {
+ locale.textdomain('app');
+
+ locale.options.locale_data = {
+ app: {
+ '': {
+ domain: 'app',
+ lang: 'vo',
+ plural_forms: 'nplurals=2; plural=(n != 1);',
+ },
+ [key]: translations,
+ },
+ };
+ };
+
+ it('translate singular text (`__`)', () => {
+ const key = 'singular';
+ const translation = 'singular_translated';
+ createTranslationMock(key, translation);
+
+ const wrapper = mount(
+ {
+ template: `
+ <span>
+ {{ __('${key}') }}
+ </span>
+ `,
+ },
+ { localVue },
+ );
+
+ expect(wrapper.text()).toBe(translation);
+ });
+
+ it('translate plural text (`n__`) without any substituting text', () => {
+ const key = 'plural';
+ const translationPlural = 'plural_multiple translation';
+ createTranslationMock(key, 'plural_singular translation', translationPlural);
+
+ const wrapper = mount(
+ {
+ template: `
+ <span>
+ {{ n__('${key}', 'plurals', 2) }}
+ </span>
+ `,
+ },
+ { localVue },
+ );
+
+ expect(wrapper.text()).toBe(translationPlural);
+ });
+
+ describe('translate plural text (`n__`) with substituting %d', () => {
+ const key = '%d day';
+
+ beforeEach(() => {
+ createTranslationMock(key, '%d singular translated', '%d plural translated');
+ });
+
+ it('and n === 1', () => {
+ const wrapper = mount(
+ {
+ template: `
+ <span>
+ {{ n__('${key}', '%d days', 1) }}
+ </span>
+ `,
+ },
+ { localVue },
+ );
+
+ expect(wrapper.text()).toBe('1 singular translated');
+ });
+
+ it('and n > 1', () => {
+ const wrapper = mount(
+ {
+ template: `
+ <span>
+ {{ n__('${key}', '%d days', 2) }}
+ </span>
+ `,
+ },
+ { localVue },
+ );
+
+ expect(wrapper.text()).toBe('2 plural translated');
+ });
+ });
+
+ describe('translates text with context `s__`', () => {
+ const key = 'Context|Foobar';
+ const translation = 'Context|Foobar translated';
+ const expectation = 'Foobar translated';
+
+ beforeEach(() => {
+ createTranslationMock(key, translation);
+ });
+
+ it('and using two parameters', () => {
+ const wrapper = mount(
+ {
+ template: `
+ <span>
+ {{ s__('Context', 'Foobar') }}
+ </span>
+ `,
+ },
+ { localVue },
+ );
+
+ expect(wrapper.text()).toBe(expectation);
+ });
+
+ it('and using the pipe syntax', () => {
+ const wrapper = mount(
+ {
+ template: `
+ <span>
+ {{ s__('${key}') }}
+ </span>
+ `,
+ },
+ { localVue },
+ );
+
+ expect(wrapper.text()).toBe(expectation);
+ });
+ });
+
+ it('translate multi line text', () => {
+ const translation = 'multiline string translated';
+ createTranslationMock('multiline string', translation);
+
+ const wrapper = mount(
+ {
+ template: `
+ <span>
+ {{ __(\`
+ multiline
+ string
+ \`) }}
+ </span>
+ `,
+ },
+ { localVue },
+ );
+
+ expect(wrapper.text()).toBe(translation);
+ });
+
+ it('translate pluralized multi line text', () => {
+ const translation = 'multiline string plural';
+
+ createTranslationMock('multiline string', 'multiline string singular', translation);
+
+ const wrapper = mount(
+ {
+ template: `
+ <span>
+ {{ n__(
+ \`
+ multiline
+ string
+ \`,
+ \`
+ multiline
+ strings
+ \`,
+ 2
+ ) }}
+ </span>
+ `,
+ },
+ { localVue },
+ );
+
+ expect(wrapper.text()).toBe(translation);
+ });
+
+ it('translate pluralized multi line text with context', () => {
+ const translation = 'multiline string with context';
+
+ createTranslationMock('Context| multiline string', translation);
+
+ const wrapper = mount(
+ {
+ template: `
+ <span>
+ {{ s__(
+ \`
+ Context|
+ multiline
+ string
+ \`
+ ) }}
+ </span>
+ `,
+ },
+ { localVue },
+ );
+
+ expect(wrapper.text()).toBe(translation);
+ });
+});
diff --git a/spec/graphql/types/metrics/dashboard_type_spec.rb b/spec/graphql/types/metrics/dashboard_type_spec.rb
index 81219c596a7..0dbd0d8b38d 100644
--- a/spec/graphql/types/metrics/dashboard_type_spec.rb
+++ b/spec/graphql/types/metrics/dashboard_type_spec.rb
@@ -7,7 +7,7 @@ describe GitlabSchema.types['MetricsDashboard'] do
it 'has the expected fields' do
expected_fields = %w[
- path annotations
+ path annotations schema_validation_warnings
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/javascripts/vue_shared/directives/tooltip_spec.js b/spec/javascripts/vue_shared/directives/tooltip_spec.js
deleted file mode 100644
index 1d516a280b0..00000000000
--- a/spec/javascripts/vue_shared/directives/tooltip_spec.js
+++ /dev/null
@@ -1,89 +0,0 @@
-import $ from 'jquery';
-import Vue from 'vue';
-import tooltip from '~/vue_shared/directives/tooltip';
-
-describe('Tooltip directive', () => {
- let vm;
-
- afterEach(() => {
- if (vm) {
- vm.$destroy();
- }
- });
-
- describe('with a single tooltip', () => {
- beforeEach(() => {
- setFixtures('<div id="dummy-element"></div>');
- vm = new Vue({
- el: '#dummy-element',
- directives: {
- tooltip,
- },
- data() {
- return {
- tooltip: 'some text',
- };
- },
- template: '<div v-tooltip :title="tooltip"></div>',
- });
- });
-
- it('should have tooltip plugin applied', () => {
- expect($(vm.$el).data('bs.tooltip')).toBeDefined();
- });
-
- it('displays the title as tooltip', () => {
- $(vm.$el).tooltip('show');
- const tooltipElement = document.querySelector('.tooltip-inner');
-
- expect(tooltipElement.innerText).toContain('some text');
- });
-
- it('updates a visible tooltip', done => {
- $(vm.$el).tooltip('show');
- const tooltipElement = document.querySelector('.tooltip-inner');
-
- vm.tooltip = 'other text';
-
- Vue.nextTick()
- .then(() => {
- expect(tooltipElement).toContainText('other text');
- done();
- })
- .catch(done.fail);
- });
- });
-
- describe('with multiple tooltips', () => {
- beforeEach(() => {
- const SomeComponent = Vue.extend({
- directives: {
- tooltip,
- },
- template: `
- <div>
- <div
- v-tooltip
- class="js-look-for-tooltip"
- title="foo">
- </div>
- <div
- v-tooltip
- title="bar">
- </div>
- </div>
- `,
- });
-
- vm = new SomeComponent().$mount();
- });
-
- it('should have tooltip plugin applied to all instances', () => {
- expect(
- $(vm.$el)
- .find('.js-look-for-tooltip')
- .data('bs.tooltip'),
- ).toBeDefined();
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/translate_spec.js b/spec/javascripts/vue_shared/translate_spec.js
deleted file mode 100644
index adca7cd64a1..00000000000
--- a/spec/javascripts/vue_shared/translate_spec.js
+++ /dev/null
@@ -1,251 +0,0 @@
-import Vue from 'vue';
-import Jed from 'jed';
-
-import { trimText } from 'spec/helpers/text_helper';
-import locale from '~/locale';
-import Translate from '~/vue_shared/translate';
-
-describe('Vue translate filter', () => {
- let el;
-
- const createTranslationMock = (key, ...translations) => {
- const fakeLocale = new Jed({
- domain: 'app',
- locale_data: {
- app: {
- '': {
- domain: 'app',
- lang: 'vo',
- plural_forms: 'nplurals=2; plural=(n != 1);',
- },
- [key]: translations,
- },
- },
- });
-
- // eslint-disable-next-line no-underscore-dangle
- locale.__Rewire__('locale', fakeLocale);
- };
-
- afterEach(() => {
- // eslint-disable-next-line no-underscore-dangle
- locale.__ResetDependency__('locale');
- });
-
- beforeEach(() => {
- Vue.use(Translate);
-
- el = document.createElement('div');
-
- document.body.appendChild(el);
- });
-
- it('translate singular text (`__`)', done => {
- const key = 'singular';
- const translation = 'singular_translated';
- createTranslationMock(key, translation);
-
- const vm = new Vue({
- el,
- template: `
- <span>
- {{ __('${key}') }}
- </span>
- `,
- }).$mount();
-
- Vue.nextTick(() => {
- expect(trimText(vm.$el.textContent)).toBe(translation);
-
- done();
- });
- });
-
- it('translate plural text (`n__`) without any substituting text', done => {
- const key = 'plural';
- const translationPlural = 'plural_multiple translation';
- createTranslationMock(key, 'plural_singular translation', translationPlural);
-
- const vm = new Vue({
- el,
- template: `
- <span>
- {{ n__('${key}', 'plurals', 2) }}
- </span>
- `,
- }).$mount();
-
- Vue.nextTick(() => {
- expect(trimText(vm.$el.textContent)).toBe(translationPlural);
-
- done();
- });
- });
-
- describe('translate plural text (`n__`) with substituting %d', () => {
- const key = '%d day';
-
- beforeEach(() => {
- createTranslationMock(key, '%d singular translated', '%d plural translated');
- });
-
- it('and n === 1', done => {
- const vm = new Vue({
- el,
- template: `
- <span>
- {{ n__('${key}', '%d days', 1) }}
- </span>
- `,
- }).$mount();
-
- Vue.nextTick(() => {
- expect(trimText(vm.$el.textContent)).toBe('1 singular translated');
-
- done();
- });
- });
-
- it('and n > 1', done => {
- const vm = new Vue({
- el,
- template: `
- <span>
- {{ n__('${key}', '%d days', 2) }}
- </span>
- `,
- }).$mount();
-
- Vue.nextTick(() => {
- expect(trimText(vm.$el.textContent)).toBe('2 plural translated');
-
- done();
- });
- });
- });
-
- describe('translates text with context `s__`', () => {
- const key = 'Context|Foobar';
- const translation = 'Context|Foobar translated';
- const expectation = 'Foobar translated';
-
- beforeEach(() => {
- createTranslationMock(key, translation);
- });
-
- it('and using two parameters', done => {
- const vm = new Vue({
- el,
- template: `
- <span>
- {{ s__('Context', 'Foobar') }}
- </span>
- `,
- }).$mount();
-
- Vue.nextTick(() => {
- expect(trimText(vm.$el.textContent)).toBe(expectation);
-
- done();
- });
- });
-
- it('and using the pipe syntax', done => {
- const vm = new Vue({
- el,
- template: `
- <span>
- {{ s__('${key}') }}
- </span>
- `,
- }).$mount();
-
- Vue.nextTick(() => {
- expect(trimText(vm.$el.textContent)).toBe(expectation);
-
- done();
- });
- });
- });
-
- it('translate multi line text', done => {
- const translation = 'multiline string translated';
- createTranslationMock('multiline string', translation);
-
- const vm = new Vue({
- el,
- template: `
- <span>
- {{ __(\`
- multiline
- string
- \`) }}
- </span>
- `,
- }).$mount();
-
- Vue.nextTick(() => {
- expect(trimText(vm.$el.textContent)).toBe(translation);
-
- done();
- });
- });
-
- it('translate pluralized multi line text', done => {
- const translation = 'multiline string plural';
-
- createTranslationMock('multiline string', 'multiline string singular', translation);
-
- const vm = new Vue({
- el,
- template: `
- <span>
- {{ n__(
- \`
- multiline
- string
- \`,
- \`
- multiline
- strings
- \`,
- 2
- ) }}
- </span>
- `,
- }).$mount();
-
- Vue.nextTick(() => {
- expect(trimText(vm.$el.textContent)).toBe(translation);
-
- done();
- });
- });
-
- it('translate pluralized multi line text with context', done => {
- const translation = 'multiline string with context';
-
- createTranslationMock('Context| multiline string', translation);
-
- const vm = new Vue({
- el,
- template: `
- <span>
- {{ s__(
- \`
- Context|
- multiline
- string
- \`
- ) }}
- </span>
- `,
- }).$mount();
-
- Vue.nextTick(() => {
- expect(trimText(vm.$el.textContent)).toBe(translation);
-
- done();
- });
- });
-});
diff --git a/spec/models/performance_monitoring/prometheus_dashboard_spec.rb b/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
index 0213712bd8c..ef7298c3d8c 100644
--- a/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
+++ b/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
@@ -42,7 +42,7 @@ describe PerformanceMonitoring::PrometheusDashboard do
it 'raises error with corresponding messages', :aggregate_failures do
expect { subject }.to raise_error do |error|
expect(error).to be_kind_of(ActiveModel::ValidationError)
- expect(error.model.errors.messages).to eql(errors_messages)
+ expect(error.model.errors.messages).to eq(errors_messages)
end
end
end
@@ -190,20 +190,51 @@ describe PerformanceMonitoring::PrometheusDashboard do
dashboard_instance = described_class.find_for(project: project, user: user, path: path, options: { environment: environment })
expect(dashboard_instance).to be_instance_of described_class
- expect(dashboard_instance.environment).to be environment
- expect(dashboard_instance.path).to be path
+ expect(dashboard_instance.environment).to eq environment
+ expect(dashboard_instance.path).to eq path
end
end
context 'dashboard has NOT been found' do
it 'returns nil' do
- allow(Gitlab::Metrics::Dashboard::Finder).to receive(:find).and_return(status: :error)
+ allow(Gitlab::Metrics::Dashboard::Finder).to receive(:find).and_return(http_status: :not_found)
dashboard_instance = described_class.find_for(project: project, user: user, path: path, options: { environment: environment })
expect(dashboard_instance).to be_nil
end
end
+
+ context 'dashboard has invalid schema', :aggregate_failures do
+ it 'still returns dashboard object' do
+ expect(Gitlab::Metrics::Dashboard::Finder).to receive(:find).and_return(http_status: :unprocessable_entity)
+
+ dashboard_instance = described_class.find_for(project: project, user: user, path: path, options: { environment: environment })
+
+ expect(dashboard_instance).to be_instance_of described_class
+ expect(dashboard_instance.environment).to eq environment
+ expect(dashboard_instance.path).to eq path
+ end
+ end
+ end
+
+ describe '#schema_validation_warnings' do
+ context 'when schema is valid' do
+ it 'returns nil' do
+ expect(described_class).to receive(:from_json)
+ expect(described_class.new.schema_validation_warnings).to be_nil
+ end
+ end
+
+ context 'when schema is invalid' do
+ it 'returns array with errors messages' do
+ instance = described_class.new
+ instance.errors.add(:test, 'test error')
+
+ expect(described_class).to receive(:from_json).and_raise(ActiveModel::ValidationError.new(instance))
+ expect(described_class.new.schema_validation_warnings).to eq ['test: test error']
+ end
+ end
end
describe '#to_yaml' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index d7c4695203d..787d778b483 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -3726,7 +3726,7 @@ describe Project do
context 'when feature is private' do
let(:project) { create(:project, :public, :merge_requests_private) }
- context 'when user does not has access to the feature' do
+ context 'when user does not have access to the feature' do
it 'does not return projects with the project feature private' do
is_expected.not_to include(project)
end
diff --git a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
index 8b0965a815b..d9d9ea9ad61 100644
--- a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
+++ b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
@@ -9,25 +9,19 @@ describe 'Getting Metrics Dashboard' do
let(:project) { create(:project) }
let!(:environment) { create(:environment, project: project) }
- let(:fields) do
- <<~QUERY
- #{all_graphql_fields_for('MetricsDashboard'.classify)}
- QUERY
- end
-
let(:query) do
- %(
- query {
- project(fullPath:"#{project.full_path}") {
- environments(name: "#{environment.name}") {
- nodes {
- metricsDashboard(path: "#{path}"){
- #{fields}
- }
- }
- }
- }
- }
+ graphql_query_for(
+ 'project', { 'fullPath' => project.full_path },
+ query_graphql_field(
+ :environments, { 'name' => environment.name },
+ query_graphql_field(
+ :nodes, nil,
+ query_graphql_field(
+ :metricsDashboard, { 'path' => path },
+ all_graphql_fields_for('MetricsDashboard'.classify)
+ )
+ )
+ )
)
end
@@ -63,7 +57,29 @@ describe 'Getting Metrics Dashboard' do
it 'returns metrics dashboard' do
dashboard = graphql_data.dig('project', 'environments', 'nodes')[0]['metricsDashboard']
- expect(dashboard).to eql("path" => path)
+ expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil)
+ end
+
+ context 'invalid dashboard' do
+ let(:path) { '.gitlab/dashboards/metrics.yml' }
+ let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndasboard: ''" }) }
+
+ it 'returns metrics dashboard' do
+ dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
+
+ expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: can't be blank"])
+ end
+ end
+
+ context 'empty dashboard' do
+ let(:path) { '.gitlab/dashboards/metrics.yml' }
+ let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) }
+
+ it 'returns metrics dashboard' do
+ dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
+
+ expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: can't be blank"])
+ end
end
end
@@ -72,7 +88,7 @@ describe 'Getting Metrics Dashboard' do
it_behaves_like 'a working graphql query'
- it 'return snil' do
+ it 'returns nil' do
dashboard = graphql_data.dig('project', 'environments', 'nodes')[0]['metricsDashboard']
expect(dashboard).to be_nil
diff --git a/spec/serializers/provider_repo_serializer_spec.rb b/spec/serializers/provider_repo_serializer_spec.rb
deleted file mode 100644
index f2be30c36d9..00000000000
--- a/spec/serializers/provider_repo_serializer_spec.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe ProviderRepoSerializer do
- it 'represents ProviderRepoEntity entities' do
- expect(described_class.entity_class).to eq(ProviderRepoEntity)
- end
-end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index bc24de9e1d3..90c53d4a346 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -38,7 +38,8 @@ describe Ci::RetryBuildService do
job_artifacts_codequality job_artifacts_metrics scheduled_at
job_variables waiting_for_resource_at job_artifacts_metrics_referee
job_artifacts_network_referee job_artifacts_dotenv
- job_artifacts_cobertura needs job_artifacts_accessibility].freeze
+ job_artifacts_cobertura needs job_artifacts_accessibility
+ job_artifacts_requirements].freeze
ignore_accessors =
%i[type lock_version target_url base_tags trace_sections
diff --git a/yarn.lock b/yarn.lock
index d1452bd6f0a..dac814f277a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -787,10 +787,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.137.0.tgz#49fb1f33340cfdf0a47c83b7a613e3c7306dd53c"
integrity sha512-dhyiedyTKYJt/mXV+PjfY2pivAAPh3BAOHpVzNCZj6HmJ9VZFIJDzOAQTTxlxRz4UyPmHPuCiaal63q+JfLzcQ==
-"@gitlab/ui@16.2.1":
- version "16.2.1"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-16.2.1.tgz#52d1a7e21b36e8cdb2766a945d928c8815956449"
- integrity sha512-JPkJUp9iyhS+u465qEMcVn9ZhZC7LqXjFBsTpF+plY9LuQWxNSEV9LU1WJ6lR1UsyNAQCJJ04qt9VgkxG52S7Q==
+"@gitlab/ui@16.3.0":
+ version "16.3.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-16.3.0.tgz#033864b3e5c3305a2e1663d2d818388fb700dedb"
+ integrity sha512-iUMKS4AMY7cBP/BHACNWA5dFcOtHL2oFu4jgeCwpI+WtJGELkGcCRKYmmErjSZ+j4nYYMT+FZnyl+Qj7xBn6+g==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"