diff options
86 files changed, 931 insertions, 330 deletions
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml index be983b177b2..3484738ebf0 100644 --- a/.gitlab/ci/frontend.gitlab-ci.yml +++ b/.gitlab/ci/frontend.gitlab-ci.yml @@ -140,13 +140,7 @@ retrieve-frontend-fixtures: script: - source scripts/gitlab_component_helpers.sh - | - if [[ -d "tmp/tests/frontend" ]]; then - # Remove tmp/tests/frontend/ except on the first parallelized job so that depending - # jobs don't download the exact same artifact multiple times. - if [[ -n "${CI_NODE_INDEX:-}" ]] && [[ "${CI_NODE_INDEX}" -ne 1 ]]; then - echoinfo "INFO: Removing 'tmp/tests/frontend' as we're on node ${CI_NODE_INDEX}."; - rm -rf "tmp/tests/frontend"; - fi + if check_fixtures_reuse; then exit 0 else echo "No frontend fixtures directory, generating frontend fixtures." diff --git a/app/assets/javascripts/import_entities/components/group_dropdown.vue b/app/assets/javascripts/import_entities/components/group_dropdown.vue index 5b9e80f9d68..1c31c04a416 100644 --- a/app/assets/javascripts/import_entities/components/group_dropdown.vue +++ b/app/assets/javascripts/import_entities/components/group_dropdown.vue @@ -4,7 +4,7 @@ import { debounce } from 'lodash'; import { s__ } from '~/locale'; import { createAlert } from '~/alert'; -import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql'; +import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql'; import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; @@ -28,7 +28,7 @@ export default { }, apollo: { namespaces: { - query: searchNamespacesWhereUserCanCreateProjectsQuery, + query: searchNamespacesWhereUserCanImportProjectsQuery, variables() { return { search: this.searchTerm, diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue index 2e6e7cddf8f..246d27d3b94 100644 --- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue +++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue @@ -24,7 +24,7 @@ import { getGroupPathAvailability } from '~/rest_api'; import axios from '~/lib/utils/axios_utils'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { helpPagePath } from '~/helpers/help_page_helper'; -import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql'; +import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { STATUSES } from '../../constants'; @@ -118,7 +118,7 @@ export default { }, }, availableNamespaces: { - query: searchNamespacesWhereUserCanCreateProjectsQuery, + query: searchNamespacesWhereUserCanImportProjectsQuery, update(data) { return data.currentUser.groups.nodes; }, diff --git a/app/assets/javascripts/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql b/app/assets/javascripts/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql new file mode 100644 index 00000000000..8c41f7116b3 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql @@ -0,0 +1,18 @@ +query searchNamespacesWhereUserCanImportProjects($search: String) { + currentUser { + id + groups(permissionScope: IMPORT_PROJECTS, search: $search) { + nodes { + id + fullPath + name + visibility + webUrl + } + } + namespace { + id + fullPath + } + } +} diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js index 61531880842..4d2df9e3602 100644 --- a/app/assets/javascripts/issues/index.js +++ b/app/assets/javascripts/issues/index.js @@ -47,7 +47,7 @@ export function initForm() { mountMilestoneDropdown(); } -export function initShow() { +export function initShow({ notesParams } = {}) { const el = document.getElementById('js-issuable-app'); if (!el) { @@ -71,7 +71,7 @@ export function initShow() { new ZenMode(); // eslint-disable-line no-new initIssuableHeaderWarnings(store); initIssuableSidebar(); - initNotesApp(); + initNotesApp(notesParams); initRelatedMergeRequests(); initSentryErrorStackTrace(); diff --git a/app/assets/javascripts/ml/experiment_tracking/constants.js b/app/assets/javascripts/ml/experiment_tracking/constants.js index 11cf321ad51..f18fbc7e2cd 100644 --- a/app/assets/javascripts/ml/experiment_tracking/constants.js +++ b/app/assets/javascripts/ml/experiment_tracking/constants.js @@ -1,7 +1,5 @@ import { s__ } from '~/locale'; -export const EMPTY_STATE_SVG = '/assets/illustrations/empty-state/empty-dag-md.svg'; - export const FEATURE_NAME = s__('MlExperimentTracking|Machine learning experiment tracking'); export const FEATURE_FEEDBACK_ISSUE = 'https://gitlab.com/gitlab-org/gitlab/-/issues/381660'; diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue index 4f2b8db3c00..eb78c65fd2a 100644 --- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue +++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue @@ -2,11 +2,7 @@ import { GlTableLite, GlEmptyState, GlLink } from '@gitlab/ui'; import IncubationAlert from '~/vue_shared/components/incubation/incubation_alert.vue'; import Pagination from '~/vue_shared/components/incubation/pagination.vue'; -import { - FEATURE_NAME, - FEATURE_FEEDBACK_ISSUE, - EMPTY_STATE_SVG, -} from '~/ml/experiment_tracking/constants'; +import { FEATURE_NAME, FEATURE_FEEDBACK_ISSUE } from '~/ml/experiment_tracking/constants'; import * as constants from '~/ml/experiment_tracking/routes/experiments/index/constants'; import * as translations from '~/ml/experiment_tracking/routes/experiments/index/translations'; @@ -28,6 +24,10 @@ export default { type: Object, required: true, }, + emptyStateSvgPath: { + type: String, + required: true, + }, }, tableFields: constants.EXPERIMENTS_TABLE_FIELDS, i18n: translations, @@ -45,7 +45,6 @@ export default { constants: { FEATURE_NAME, FEATURE_FEEDBACK_ISSUE, - EMPTY_STATE_SVG, ...constants, }, }; @@ -78,7 +77,7 @@ export default { :title="$options.i18n.EMPTY_STATE_TITLE_LABEL" :primary-button-text="$options.i18n.CREATE_NEW_LABEL" :primary-button-link="$options.constants.CREATE_EXPERIMENT_HELP_PATH" - :svg-path="$options.constants.EMPTY_STATE_SVG" + :svg-path="emptyStateSvgPath" :description="$options.i18n.EMPTY_STATE_DESCRIPTION_LABEL" class="gl-py-8" /> diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue index acb5fc7cad2..e8118867f7d 100644 --- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue +++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue @@ -3,11 +3,7 @@ import { GlTableLite, GlLink, GlEmptyState } from '@gitlab/ui'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue'; import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; -import { - FEATURE_NAME, - FEATURE_FEEDBACK_ISSUE, - EMPTY_STATE_SVG, -} from '~/ml/experiment_tracking/constants'; +import { FEATURE_NAME, FEATURE_FEEDBACK_ISSUE } from '~/ml/experiment_tracking/constants'; import { queryToObject, setUrlParams, visitUrl } from '~/lib/utils/url_utility'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import KeysetPagination from '~/vue_shared/components/incubation/pagination.vue'; @@ -54,6 +50,10 @@ export default { type: Object, required: true, }, + emptyStateSvgPath: { + type: String, + required: true, + }, }, data() { const query = queryToObject(window.location.search); @@ -159,7 +159,6 @@ export default { FEATURE_NAME, FEATURE_FEEDBACK_ISSUE, CREATE_CANDIDATE_HELP_PATH, - EMPTY_STATE_SVG, }, }; </script> @@ -227,7 +226,7 @@ export default { :title="$options.i18n.EMPTY_STATE_TITLE_LABEL" :primary-button-text="$options.i18n.CREATE_NEW_LABEL" :primary-button-link="$options.constants.CREATE_CANDIDATE_HELP_PATH" - :svg-path="$options.constants.EMPTY_STATE_SVG" + :svg-path="emptyStateSvgPath" :description="$options.i18n.EMPTY_STATE_DESCRIPTION_LABEL" class="gl-py-8" /> diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index cf7207d260d..a499d94db39 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -1,14 +1,13 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { apolloProvider } from '~/graphql_shared/issuable_client'; -import { convertToGraphQLId } from '~/graphql_shared/utils'; import { parseBoolean } from '~/lib/utils/common_utils'; import { getLocationHash } from '~/lib/utils/url_utility'; import NotesApp from './components/notes_app.vue'; import { store } from './stores'; import { getNotesFilterData } from './utils/get_notes_filter_data'; -export default () => { +export default ({ editorAiActions = [] } = {}) => { const el = document.getElementById('js-vue-notes'); if (!el) { return; @@ -60,7 +59,7 @@ export default () => { showTimelineViewToggle, reportAbusePath: notesDataset.reportAbusePath, newCommentTemplatePath: notesDataset.newCommentTemplatePath, - resourceGlobalId: convertToGraphQLId(noteableData.noteableType, noteableData.id), + editorAiActions: editorAiActions.map((factory) => factory(notesDataset)), }, data() { return { diff --git a/app/assets/javascripts/pages/admin/jobs/components/constants.js b/app/assets/javascripts/pages/admin/jobs/components/constants.js index 84be895e194..61d5e329fc0 100644 --- a/app/assets/javascripts/pages/admin/jobs/components/constants.js +++ b/app/assets/javascripts/pages/admin/jobs/components/constants.js @@ -1,5 +1,5 @@ import { s__, __ } from '~/locale'; -import { DEFAULT_FIELDS } from '~/jobs/components/table/constants'; +import { DEFAULT_FIELDS, RAW_TEXT_WARNING } from '~/jobs/components/table/constants'; export const CANCEL_JOBS_MODAL_ID = 'cancel-jobs-modal'; export const CANCEL_JOBS_MODAL_TITLE = s__('AdminArea|Are you sure?'); @@ -19,3 +19,5 @@ export const DEFAULT_FIELDS_ADMIN = [ { key: 'runner', label: __('Runner'), columnClass: 'gl-w-15p' }, ...DEFAULT_FIELDS.slice(2), ]; + +export const RAW_TEXT_WARNING_ADMIN = RAW_TEXT_WARNING; diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue b/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue index 7057e71aefe..da6739aad8b 100644 --- a/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue +++ b/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue @@ -1,13 +1,15 @@ <script> import { GlAlert, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui'; import { __ } from '~/locale'; -import { queryToObject } from '~/lib/utils/url_utility'; +import { setUrlParams, updateHistory, queryToObject } from '~/lib/utils/url_utility'; import { validateQueryString } from '~/jobs/components/filtered_search/utils'; import JobsTable from '~/jobs/components/table/jobs_table.vue'; import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue'; +import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue'; import JobsTableEmptyState from '~/jobs/components/table/jobs_table_empty_state.vue'; -import { DEFAULT_FIELDS_ADMIN } from '../constants'; +import { createAlert } from '~/alert'; import JobsSkeletonLoader from '../jobs_skeleton_loader.vue'; +import { DEFAULT_FIELDS_ADMIN, RAW_TEXT_WARNING_ADMIN } from '../constants'; import GetAllJobs from './graphql/queries/get_all_jobs.query.graphql'; import CancelableJobs from './graphql/queries/get_cancelable_jobs_count.query.graphql'; @@ -16,10 +18,13 @@ export default { jobsFetchErrorMsg: __('There was an error fetching the jobs.'), loadingAriaLabel: __('Loading'), }, + filterSearchBoxStyles: + 'gl-my-0 gl-p-5 gl-bg-gray-10 gl-text-gray-900 gl-border-b gl-border-gray-100', components: { JobsSkeletonLoader, JobsTableEmptyState, GlAlert, + JobsFilteredSearch, JobsTable, JobsTableTabs, GlIntersectionObserver, @@ -101,6 +106,9 @@ export default { return validateQueryString(queryStringObject); }, + showFilteredSearch() { + return !this.scope; + }, jobsCount() { return this.jobs.count; }, @@ -140,6 +148,44 @@ export default { }); } }, + filterJobsBySearch(filters) { + this.infiniteScrollingTriggered = false; + this.filterSearchTriggered = true; + + // all filters have been cleared reset query param + // and refetch jobs/count with defaults + if (!filters.length) { + updateHistory({ + url: setUrlParams({ statuses: null }, window.location.href, true), + }); + + this.$apollo.queries.jobs.refetch({ statuses: null }); + + return; + } + + // Eventually there will be more tokens available + // this code is written to scale for those tokens + filters.forEach((filter) => { + // Raw text input in filtered search does not have a type + // when a user enters raw text we alert them that it is + // not supported and we do not make an additional API call + if (!filter.type) { + createAlert({ + message: RAW_TEXT_WARNING_ADMIN, + type: 'warning', + }); + } + + if (filter.type === 'status') { + updateHistory({ + url: setUrlParams({ statuses: filter.value.data }, window.location.href, true), + }); + + this.$apollo.queries.jobs.refetch({ statuses: filter.value.data }); + } + }); + }, }, }; </script> @@ -157,6 +203,13 @@ export default { @fetchJobsByStatus="fetchJobsByStatus" /> + <div v-if="showFilteredSearch" :class="$options.filterSearchBoxStyles"> + <jobs-filtered-search + :query-string="validatedQueryString" + @filterJobsBySearch="filterJobsBySearch" + /> + </div> + <jobs-skeleton-loader v-if="showSkeletonLoader" class="gl-mt-5" /> <jobs-table-empty-state v-else-if="showEmptyState" /> diff --git a/app/assets/javascripts/pages/projects/ml/experiments/index/index.js b/app/assets/javascripts/pages/projects/ml/experiments/index/index.js index e9ffd4b528b..b054022b6d6 100644 --- a/app/assets/javascripts/pages/projects/ml/experiments/index/index.js +++ b/app/assets/javascripts/pages/projects/ml/experiments/index/index.js @@ -8,9 +8,11 @@ const initIndexMlExperiments = () => { return undefined; } + const { experiments, pageInfo, emptyStateSvgPath } = element.dataset; const props = { - experiments: JSON.parse(element.dataset.experiments), - pageInfo: convertObjectPropsToCamelCase(JSON.parse(element.dataset.pageInfo)), + experiments: JSON.parse(experiments), + pageInfo: convertObjectPropsToCamelCase(JSON.parse(pageInfo)), + emptyStateSvgPath, }; return new Vue({ diff --git a/app/assets/javascripts/pages/projects/ml/experiments/show/index.js b/app/assets/javascripts/pages/projects/ml/experiments/show/index.js index f50763151ef..b3f15a9f65e 100644 --- a/app/assets/javascripts/pages/projects/ml/experiments/show/index.js +++ b/app/assets/javascripts/pages/projects/ml/experiments/show/index.js @@ -8,12 +8,15 @@ const initShowExperiment = () => { return undefined; } + const { experiment, candidates, metrics, params, pageInfo, emptyStateSvgPath } = element.dataset; + const props = { - experiment: JSON.parse(element.dataset.experiment), - candidates: JSON.parse(element.dataset.candidates), - metricNames: JSON.parse(element.dataset.metrics), - paramNames: JSON.parse(element.dataset.params), - pageInfo: convertObjectPropsToCamelCase(JSON.parse(element.dataset.pageInfo)), + experiment: JSON.parse(experiment), + candidates: JSON.parse(candidates), + metricNames: JSON.parse(metrics), + paramNames: JSON.parse(params), + pageInfo: convertObjectPropsToCamelCase(JSON.parse(pageInfo)), + emptyStateSvgPath, }; return new Vue({ diff --git a/app/assets/javascripts/projects/new/components/app.vue b/app/assets/javascripts/projects/new/components/app.vue index 0a160a357e5..2f58d4468be 100644 --- a/app/assets/javascripts/projects/new/components/app.vue +++ b/app/assets/javascripts/projects/new/components/app.vue @@ -9,6 +9,7 @@ import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue' import NewProjectPushTipPopover from './new_project_push_tip_popover.vue'; const CI_CD_PANEL = 'cicd_for_external_repo'; +const IMPORT_PROJECT_PANEL = 'import_project'; const PANELS = [ { key: 'blank', @@ -32,7 +33,7 @@ const PANELS = [ }, { key: 'import', - name: 'import_project', + name: IMPORT_PROJECT_PANEL, selector: '#import-project-pane', title: s__('ProjectsNew|Import project'), description: s__( @@ -92,6 +93,11 @@ export default { required: false, default: '', }, + canImportProjects: { + type: Boolean, + required: false, + default: true, + }, }, computed: { @@ -106,7 +112,21 @@ export default { return breadcrumbs; }, availablePanels() { - return this.isCiCdAvailable ? PANELS : PANELS.filter((p) => p.name !== CI_CD_PANEL); + if (this.isCiCdAvailable && this.canImportProjects) { + return PANELS; + } + + return PANELS.filter((panel) => { + if (!this.canImportProjects && panel.name === IMPORT_PROJECT_PANEL) { + return false; + } + + if (!this.isCiCdAvailable && panel.name === CI_CD_PANEL) { + return false; + } + + return true; + }); }, }, diff --git a/app/assets/javascripts/projects/new/index.js b/app/assets/javascripts/projects/new/index.js index 5ec50355a82..a5a833dc73b 100644 --- a/app/assets/javascripts/projects/new/index.js +++ b/app/assets/javascripts/projects/new/index.js @@ -19,6 +19,7 @@ export function initNewProjectCreation() { parentGroupName, projectsUrl, rootPath, + canImportProjects, } = el.dataset; const props = { @@ -29,6 +30,7 @@ export function initNewProjectCreation() { parentGroupName, projectsUrl, rootPath, + canImportProjects: parseBoolean(canImportProjects), }; const provide = { diff --git a/app/assets/javascripts/vue_shared/alert_details/router.js b/app/assets/javascripts/vue_shared/alert_details/router.js index 26477a3a66a..616d5c259b9 100644 --- a/app/assets/javascripts/vue_shared/alert_details/router.js +++ b/app/assets/javascripts/vue_shared/alert_details/router.js @@ -5,26 +5,9 @@ import { joinPaths } from '~/lib/utils/url_utility'; Vue.use(VueRouter); export default function createRouter(base) { - const router = new VueRouter({ + return new VueRouter({ mode: 'history', base: joinPaths(gon.relative_url_root || '', base), routes: [{ path: '/:tabId', name: 'tab' }], }); - - /* - Backward-compatible behavior. Redirects hash mode URLs to history mode ones. - Ex: from #/overview to /overview - from #/metrics to /metrics - from #/activity to /activity - */ - router.beforeEach((to, _, next) => { - if (to.hash.startsWith('#/')) { - const path = to.fullPath.substring(2); - next(path); - } else { - next(); - } - }); - - return router; } diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 3486f231b39..75bb622234a 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -15,6 +15,7 @@ import { getModifierKey } from '~/constants'; import { getSelectedFragment } from '~/lib/utils/common_utils'; import { s__, __ } from '~/locale'; import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm'; +import { updateText } from '~/lib/utils/text_markdown'; import ToolbarButton from './toolbar_button.vue'; import DrawioToolbarButton from './drawio_toolbar_button.vue'; import CommentTemplatesDropdown from './comment_templates_dropdown.vue'; @@ -39,7 +40,7 @@ export default { newCommentTemplatePath: { default: null, }, - resourceGlobalId: { default: null }, + editorAiActions: { default: () => [] }, }, props: { previewMarkdown: { @@ -121,9 +122,6 @@ export default { const expandText = s__('MarkdownEditor|Click to expand'); return [`<details><summary>${expandText}</summary>`, `{text}`, '</details>'].join('\n'); }, - showAiActions() { - return this.resourceGlobalId && this.glFeatures.summarizeComments; - }, }, watch: { showSuggestPopover() { @@ -195,6 +193,18 @@ export default { $gfmForm.find('.div-dropzone').click(); $gfmTextarea.focus(); }, + insertIntoTextarea(text) { + const textArea = this.$el.closest('.md-area')?.querySelector('textarea'); + if (textArea) { + const generatedByText = `${text}\n***\n_${__('This comment was generated using OpenAI')}_`; + updateText({ + textArea, + tag: generatedByText, + cursorOffset: 0, + wrap: false, + }); + } + }, }, shortcuts: { bold: keysFor(BOLD_TEXT), @@ -275,7 +285,11 @@ export default { </gl-button> </gl-popover> </template> - <ai-actions-dropdown v-if="showAiActions" :resource-global-id="resourceGlobalId" /> + <ai-actions-dropdown + v-if="editorAiActions.length" + :actions="editorAiActions" + @input="insertIntoTextarea" + /> <toolbar-button tag="**" :button-title=" diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index 7ef07032913..bcb6aed9e38 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -18,7 +18,7 @@ class Import::BaseController < ApplicationController if params[:namespace_id]&.present? @namespace = Namespace.find_by_id(params[:namespace_id]) - render_404 unless current_user.can?(:create_projects, @namespace) + render_404 unless current_user.can?(:import_projects, @namespace) end end end diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 8a0f4a36781..c933b05e0c4 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -57,7 +57,7 @@ class Import::BitbucketController < Import::BaseController extra: { user_role: user_role(current_user, target_namespace), import_type: 'bitbucket' } ) - if current_user.can?(:create_projects, target_namespace) + if current_user.can?(:import_projects, target_namespace) # The token in a session can be expired, we need to get most recent one because # Bitbucket::Connection class refreshes it. session[:bitbucket_token] = bitbucket_client.connection.token @@ -70,7 +70,7 @@ class Import::BitbucketController < Import::BaseController render json: { errors: project_save_error(project) }, status: :unprocessable_entity end else - render json: { errors: _('This namespace has already been taken! Please choose another one.') }, status: :unprocessable_entity + render json: { errors: _('You are not allowed to import projects in this namespace.') }, status: :unprocessable_entity end end diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb index 047c273969c..2778b97419a 100644 --- a/app/controllers/import/gitea_controller.rb +++ b/app/controllers/import/gitea_controller.rb @@ -32,7 +32,7 @@ class Import::GiteaController < Import::GithubController if params[:namespace_id].present? @namespace = Namespace.find_by_id(params[:namespace_id]) - render_404 unless current_user.can?(:create_projects, @namespace) + render_404 unless current_user.can?(:import_projects, @namespace) end end end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index bd0c0976729..719cd61e538 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -65,7 +65,7 @@ class Import::GithubController < Import::BaseController if params[:namespace_id].present? @namespace = Namespace.find_by_id(params[:namespace_id]) - render_404 unless current_user.can?(:create_projects, @namespace) + render_404 unless current_user.can?(:import_projects, @namespace) end end end diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index 9b8c480e529..d1b182a57d8 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -8,7 +8,7 @@ class Import::GitlabProjectsController < Import::BaseController def new @namespace = Namespace.find(project_params[:namespace_id]) - return render_404 unless current_user.can?(:create_projects, @namespace) + return render_404 unless current_user.can?(:import_projects, @namespace) @path = project_params[:path] end diff --git a/app/controllers/import/manifest_controller.rb b/app/controllers/import/manifest_controller.rb index 461ba982969..03884717e54 100644 --- a/app/controllers/import/manifest_controller.rb +++ b/app/controllers/import/manifest_controller.rb @@ -20,8 +20,8 @@ class Import::ManifestController < Import::BaseController def upload group = Group.find(params[:group_id]) - unless can?(current_user, :create_projects, group) - @errors = ["You don't have enough permissions to create projects in the selected group"] + unless can?(current_user, :import_projects, group) + @errors = ["You don't have enough permissions to import projects in the selected group"] render :new && return end diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index 41daeddcf7f..208fbc40556 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -56,7 +56,7 @@ class Projects::ImportsController < Projects::ApplicationController end def require_namespace_project_creation_permission - render_404 unless can?(current_user, :admin_project, @project) || can?(current_user, :create_projects, @project.namespace) + render_404 unless can?(current_user, :admin_project, @project) || can?(current_user, :import_projects, @project.namespace) end def redirect_if_progress diff --git a/app/finders/groups/accepting_project_imports_finder.rb b/app/finders/groups/accepting_project_imports_finder.rb new file mode 100644 index 00000000000..55d72edf7bb --- /dev/null +++ b/app/finders/groups/accepting_project_imports_finder.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Groups + class AcceptingProjectImportsFinder + def initialize(current_user) + @current_user = current_user + end + + def execute + ::Group.from_union( + [ + current_user.manageable_groups, + managable_groups_originating_from_group_shares + ] + ) + end + + private + + attr_reader :current_user + + def managable_groups_originating_from_group_shares + GroupGroupLink + .with_owner_or_maintainer_access + .groups_accessible_via( + current_user.owned_or_maintainers_groups + .select(:id) + ) + end + end +end diff --git a/app/finders/groups/user_groups_finder.rb b/app/finders/groups/user_groups_finder.rb index 83e012b3dbe..536b81b2300 100644 --- a/app/finders/groups/user_groups_finder.rb +++ b/app/finders/groups/user_groups_finder.rb @@ -39,6 +39,8 @@ module Groups Groups::AcceptingProjectCreationsFinder.new(target_user).execute # rubocop: disable CodeReuse/Finder elsif permission_scope_transfer_projects? Groups::AcceptingProjectTransfersFinder.new(target_user).execute # rubocop: disable CodeReuse/Finder + elsif permission_scope_import_projects? + Groups::AcceptingProjectImportsFinder.new(target_user).execute # rubocop: disable CodeReuse/Finder else target_user.groups end @@ -51,5 +53,9 @@ module Groups def permission_scope_transfer_projects? params[:permission_scope] == :transfer_projects end + + def permission_scope_import_projects? + params[:permission_scope] == :import_projects + end end end diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb index 1d12c296b2e..253fee19f2e 100644 --- a/app/graphql/types/ci/job_type.rb +++ b/app/graphql/types/ci/job_type.rb @@ -7,6 +7,8 @@ module Types class JobType < BaseObject graphql_name 'CiJob' + present_using ::Ci::BuildPresenter + connection_type_class(Types::LimitedCountableConnectionType) expose_permissions Types::PermissionTypes::Ci::Job @@ -91,7 +93,7 @@ module Types description: 'Path to the ref.' field :retried, GraphQL::Types::Boolean, null: true, description: 'Indicates that the job has been retried.' - field :retryable, GraphQL::Types::Boolean, null: false, method: :retryable?, + field :retryable, GraphQL::Types::Boolean, null: false, description: 'Indicates the job can be retried.' field :scheduled, GraphQL::Types::Boolean, null: false, method: :scheduled?, description: 'Indicates the job is scheduled.' @@ -114,14 +116,21 @@ module Types null: false, resolver_method: :can_play_job?, description: 'Indicates whether the current user can play the job.' + field :failure_message, GraphQL::Types::String, null: true, + description: 'Message on why the job failed.' + def can_play_job? object.playable? && Ability.allowed?(current_user, :play_job, object) end def kind - return ::Ci::Build unless [::Ci::Build, ::Ci::Bridge].include?(object.class) + return ::Ci::Build unless [::Ci::Build, ::Ci::Bridge].include?(object.build.class) + + object.build.class + end - object.class + def retryable + object.build.retryable? end def pipeline diff --git a/app/graphql/types/permission_types/group_enum.rb b/app/graphql/types/permission_types/group_enum.rb index f636d43790f..6d51d94a70d 100644 --- a/app/graphql/types/permission_types/group_enum.rb +++ b/app/graphql/types/permission_types/group_enum.rb @@ -10,6 +10,9 @@ module Types value 'TRANSFER_PROJECTS', value: :transfer_projects, description: 'Groups where the user can transfer projects to.' + value 'IMPORT_PROJECTS', + value: :import_projects, + description: 'Groups where the user can import projects to.' end end end diff --git a/app/models/packages/npm/metadatum.rb b/app/models/packages/npm/metadatum.rb index a856cd7225f..ccbf056ec7b 100644 --- a/app/models/packages/npm/metadatum.rb +++ b/app/models/packages/npm/metadatum.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class Packages::Npm::Metadatum < ApplicationRecord + MAX_PACKAGE_JSON_SIZE = 20_000 + MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING = 5_000 + NUM_FIELDS_FOR_ERROR_TRACKING = 5 + belongs_to :package, -> { where(package_type: :npm) }, inverse_of: :npm_metadatum validates :package, presence: true @@ -20,7 +24,7 @@ class Packages::Npm::Metadatum < ApplicationRecord end def ensure_package_json_size - return if package_json.to_s.size < 20000 + return if package_json.to_s.size < MAX_PACKAGE_JSON_SIZE errors.add(:package_json, _('structure is too large')) end diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index a8eb990b914..83a8f755d61 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -180,6 +180,7 @@ class Packages::Package < ApplicationRecord scope :order_project_name, -> { joins(:project).reorder('projects.name ASC') } scope :order_project_name_desc, -> { joins(:project).reorder('projects.name DESC') } scope :order_by_package_file, -> { joins(:package_files).order('packages_package_files.created_at ASC') } + scope :with_npm_scope, ->(scope) { npm.where("name ILIKE :package_name", package_name: "@#{sanitize_sql_like(scope)}/%") } scope :order_project_path, -> do keyset_order = keyset_pagination_order(join_class: Project, column_name: :path, direction: :asc) diff --git a/app/models/project.rb b/app/models/project.rb index 7aa13a94c8f..c1f5a2315ef 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1177,10 +1177,6 @@ class Project < ApplicationRecord auto_devops_config[:scope] != :project && !auto_devops_config[:status] end - def has_packages?(package_type) - packages.where(package_type: package_type).exists? - end - def packages_cleanup_policy super || build_packages_cleanup_policy end @@ -2957,6 +2953,12 @@ class Project < ApplicationRecord ).exists? end + def has_namespaced_npm_packages? + packages.with_npm_scope(root_namespace.path) + .not_pending_destruction + .exists? + end + def default_branch_or_main return default_branch if default_branch diff --git a/app/policies/namespaces/user_namespace_policy.rb b/app/policies/namespaces/user_namespace_policy.rb index 1deeae8241f..bfed61e72d3 100644 --- a/app/policies/namespaces/user_namespace_policy.rb +++ b/app/policies/namespaces/user_namespace_policy.rb @@ -11,6 +11,7 @@ module Namespaces rule { owner | admin }.policy do enable :owner_access enable :create_projects + enable :import_projects enable :admin_namespace enable :read_namespace enable :read_statistics @@ -20,9 +21,9 @@ module Namespaces enable :edit_billing end - rule { ~can_create_personal_project }.prevent :create_projects + rule { ~can_create_personal_project }.prevent :create_projects, :import_projects - rule { bot_user_namespace }.prevent :create_projects + rule { bot_user_namespace }.prevent :create_projects, :import_projects rule { (owner | admin) & can?(:create_projects) }.enable :transfer_projects end diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb index 513fcd90cf8..b89e8db334d 100644 --- a/app/presenters/ci/build_presenter.rb +++ b/app/presenters/ci/build_presenter.rb @@ -27,6 +27,10 @@ module Ci scheduled? && scheduled_at && [0, scheduled_at - Time.now].max end + def failure_message + callout_failure_message if build.failed? + end + private def tooltip_for_badge(status) diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb index 7e9fd9dad54..1c8df157716 100644 --- a/app/services/groups/transfer_service.rb +++ b/app/services/groups/transfer_service.rb @@ -60,7 +60,7 @@ module Groups raise_transfer_error(:namespace_with_same_path) if namespace_with_same_path? raise_transfer_error(:group_contains_images) if group_projects_contain_registry_images? raise_transfer_error(:cannot_transfer_to_subgroup) if transfer_to_subgroup? - raise_transfer_error(:group_contains_npm_packages) if group_with_npm_packages? + raise_transfer_error(:group_contains_namespaced_npm_packages) if group_with_namespaced_npm_packages? raise_transfer_error(:no_permissions_to_migrate_crm) if no_permissions_to_migrate_crm? end @@ -74,10 +74,11 @@ module Groups false end - def group_with_npm_packages? + def group_with_namespaced_npm_packages? return false unless group.packages_feature_enabled? - npm_packages = ::Packages::GroupPackagesFinder.new(current_user, group, package_type: :npm).execute + npm_packages = ::Packages::GroupPackagesFinder.new(current_user, group, package_type: :npm, preload_pipelines: false).execute + npm_packages = npm_packages.with_npm_scope(group.root_ancestor.path) different_root_ancestor? && npm_packages.exists? end @@ -219,7 +220,7 @@ module Groups invalid_policies: s_("TransferGroup|You don't have enough permissions."), group_contains_images: s_('TransferGroup|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again.'), cannot_transfer_to_subgroup: s_('TransferGroup|Cannot transfer group to one of its subgroup.'), - group_contains_npm_packages: s_('TransferGroup|Group contains projects with NPM packages.'), + group_contains_namespaced_npm_packages: s_('TransferGroup|Group contains projects with NPM packages scoped to the current root level group.'), no_permissions_to_migrate_crm: s_("TransferGroup|Group contains contacts/organizations and you don't have enough permissions to move them to the new root group.") }.freeze end diff --git a/app/services/import/base_service.rb b/app/services/import/base_service.rb index 6b5adcbc39e..64cf3cfa04a 100644 --- a/app/services/import/base_service.rb +++ b/app/services/import/base_service.rb @@ -9,7 +9,7 @@ module Import end def authorized? - can?(current_user, :create_projects, target_namespace) + can?(current_user, :import_projects, target_namespace) end private diff --git a/app/services/import/bitbucket_server_service.rb b/app/services/import/bitbucket_server_service.rb index f7f17f1e53e..5d496dc7cc3 100644 --- a/app/services/import/bitbucket_server_service.rb +++ b/app/services/import/bitbucket_server_service.rb @@ -10,7 +10,7 @@ module Import end unless authorized? - return log_and_return_error("You don't have permissions to create this project", :unauthorized) + return log_and_return_error("You don't have permissions to import this project", :unauthorized) end unless repo diff --git a/app/services/import/fogbugz_service.rb b/app/services/import/fogbugz_service.rb index d1003823456..9a8def43312 100644 --- a/app/services/import/fogbugz_service.rb +++ b/app/services/import/fogbugz_service.rb @@ -13,8 +13,8 @@ module Import unless authorized? return log_and_return_error( - "You don't have permissions to create this project", - _("You don't have permissions to create this project"), + "You don't have permissions to import this project", + _("You don't have permissions to import this project"), :unauthorized ) end diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb index b30c344723d..7e7f7ea9810 100644 --- a/app/services/import/github_service.rb +++ b/app/services/import/github_service.rb @@ -103,7 +103,7 @@ module Import elsif target_namespace.nil? error(_('Namespace or group to import repository into does not exist.'), :unprocessable_entity) elsif !authorized? - error(_('This namespace has already been taken. Choose a different one.'), :unprocessable_entity) + error(_('You are not allowed to import projects in this namespace.'), :unprocessable_entity) elsif oversized? error(oversize_error_message, :unprocessable_entity) end diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb index 33a7736dc95..b5a0a22a24e 100644 --- a/app/services/packages/npm/create_package_service.rb +++ b/app/services/packages/npm/create_package_service.rb @@ -35,11 +35,21 @@ module Packages ::Packages::CreateDependencyService.new(package, package_dependencies).execute ::Packages::Npm::CreateTagService.new(package, dist_tag).execute - package.create_npm_metadatum!(package_json: package_json) + create_npm_metadatum!(package) package end + def create_npm_metadatum!(package) + package.create_npm_metadatum!(package_json: package_json) + rescue ActiveRecord::RecordInvalid => e + if package.npm_metadatum && package.npm_metadatum.errors.added?(:package_json, 'structure is too large') + Gitlab::ErrorTracking.track_exception(e, field_sizes: field_sizes_for_error_tracking) + end + + raise + end + def current_package_exists? project.packages .npm @@ -125,6 +135,35 @@ module Packages def lease_timeout DEFAULT_LEASE_TIMEOUT end + + def field_sizes + strong_memoize(:field_sizes) do + package_json.transform_values do |value| + value.to_s.size + end + end + end + + def filtered_field_sizes + strong_memoize(:filtered_field_sizes) do + field_sizes.select do |_, size| + size >= ::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING + end + end + end + + def largest_fields + strong_memoize(:largest_fields) do + field_sizes + .sort_by { |a| a[1] } + .reverse[0..::Packages::Npm::Metadatum::NUM_FIELDS_FOR_ERROR_TRACKING - 1] + .to_h + end + end + + def field_sizes_for_error_tracking + filtered_field_sizes.empty? ? largest_fields : filtered_field_sizes + end end end end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index cbea44d6aff..63b050faf9c 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -58,6 +58,7 @@ module Projects return @project if @project.errors.any? validate_create_permissions + validate_import_permissions return @project if @project.errors.any? @relations_block&.call(@project) @@ -98,6 +99,13 @@ module Projects @project.errors.add(:namespace, "is not valid") end + def validate_import_permissions + return unless @project.import? + return if current_user.can?(:import_projects, parent_namespace) + + @project.errors.add(:user, 'is not allowed to import projects') + end + def after_create_actions log_info("#{current_user.name} created a new project \"#{@project.full_name}\"") diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index ed99c69be07..4a9d96d266c 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -63,8 +63,8 @@ module Projects raise TransferError, s_('TransferProject|Project cannot be transferred, because tags are present in its container registry') end - if project.has_packages?(:npm) && !new_namespace_has_same_root?(project) - raise TransferError, s_("TransferProject|Root namespace can't be updated if project has NPM packages") + if !new_namespace_has_same_root?(project) && project.has_namespaced_npm_packages? + raise TransferError, s_("TransferProject|Root namespace can't be updated if the project has NPM packages scoped to the current root level namespace.") end proceed_to_transfer diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml index 1a0f4132d49..dde0b301c63 100644 --- a/app/views/admin/abuse_reports/_abuse_report.html.haml +++ b/app/views/admin/abuse_reports/_abuse_report.html.haml @@ -7,7 +7,7 @@ - if user = link_to user.name, user .light.small - = _('Joined %{time_ago}').html_safe % { time_ago: time_ago_with_tooltip(user.created_at) } + = html_escape(_('Joined %{time_ago}')) % { time_ago: time_ago_with_tooltip(user.created_at).html_safe } - else = _('(removed)') %td diff --git a/app/views/groups/settings/access_tokens/index.html.haml b/app/views/groups/settings/access_tokens/index.html.haml index 8435f32db49..bf78b2f8e68 100644 --- a/app/views/groups/settings/access_tokens/index.html.haml +++ b/app/views/groups/settings/access_tokens/index.html.haml @@ -12,15 +12,15 @@ - if current_user.can?(:create_resource_access_tokens, @group) = _('Generate group access tokens scoped to this group for your applications that need access to the GitLab API.') %p - = _('You can also use group access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } + = html_escape(_('You can also use group access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}')) % { link_start: link_start, link_end: '</a>'.html_safe } - else - = _('Group access token creation is disabled in this group. You can still use and manage existing tokens. %{link_start}Learn more.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } + = html_escape(_('Group access token creation is disabled in this group. You can still use and manage existing tokens. %{link_start}Learn more.%{link_end}')) % { link_start: link_start, link_end: '</a>'.html_safe } %p - root_group = @group.root_ancestor - if current_user.can?(:admin_group, root_group) - group_settings_link = edit_group_path(root_group) - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_settings_link } - = _('You can enable group access token creation in %{link_start}group settings%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } + = html_escape(_('You can enable group access token creation in %{link_start}group settings%{link_end}.')) % { link_start: link_start, link_end: '</a>'.html_safe } .col-lg-8 #js-new-access-token-app{ data: { access_token_type: type } } diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml index 67b87f842f9..389cf80f954 100644 --- a/app/views/groups/settings/ci_cd/show.html.haml +++ b/app/views/groups/settings/ci_cd/show.html.haml @@ -47,7 +47,7 @@ - quickstart_url = help_page_path('topics/autodevops/cloud_deployments/auto_devops_with_gke') - auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url } - quickstart_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: quickstart_url } - = s_('AutoDevOps|%{auto_devops_start}Automate building, testing, and deploying%{auto_devops_end} your applications based on your continuous integration and delivery configuration. %{quickstart_start}How do I get started?%{quickstart_end}').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe, quickstart_start: quickstart_start, quickstart_end: '</a>'.html_safe } + = html_escape(s_('AutoDevOps|%{auto_devops_start}Automate building, testing, and deploying%{auto_devops_end} your applications based on your continuous integration and delivery configuration. %{quickstart_start}How do I get started?%{quickstart_end}')) % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe, quickstart_start: quickstart_start, quickstart_end: '</a>'.html_safe } .settings-content = render 'groups/settings/ci_cd/auto_devops_form', group: @group diff --git a/app/views/projects/ml/experiments/index.html.haml b/app/views/projects/ml/experiments/index.html.haml index dd064239e36..612481dbf41 100644 --- a/app/views/projects/ml/experiments/index.html.haml +++ b/app/views/projects/ml/experiments/index.html.haml @@ -3,5 +3,6 @@ #js-project-ml-experiments-index{ data: { experiments: experiments_as_data(@project, @experiments), - page_info: formatted_page_info(@page_info) + page_info: formatted_page_info(@page_info), + empty_state_svg_path: image_path('illustrations/empty-state/empty-dag-md.svg'), } } diff --git a/app/views/projects/ml/experiments/show.html.haml b/app/views/projects/ml/experiments/show.html.haml index cfec627d249..d4a05b613c9 100644 --- a/app/views/projects/ml/experiments/show.html.haml +++ b/app/views/projects/ml/experiments/show.html.haml @@ -14,5 +14,6 @@ candidates: items, metrics: metrics, params: params, - page_info: page_info + page_info: page_info, + empty_state_svg_path: image_path('illustrations/empty-state/empty-dag-md.svg'), } } diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index e64ed2c7b8f..52ac8b58c9a 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -17,7 +17,8 @@ root_path: root_path, parent_group_url: @project.parent && group_url(@project.parent), parent_group_name: @project.parent&.name, - projects_url: dashboard_projects_url } } + projects_url: dashboard_projects_url, + can_import_projects: params[:namespace_id].presence ? current_user.can?(:import_projects, @namespace).to_s : 'true' } } .row{ 'v-cloak': true } #blank-project-pane.tab-pane.active diff --git a/config/feature_flags/development/exit_registration_verification.yml b/config/feature_flags/development/exit_registration_verification.yml deleted file mode 100644 index c544ebc2943..00000000000 --- a/config/feature_flags/development/exit_registration_verification.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: exit_registration_verification -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80286 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352397 -milestone: '14.8' -type: development -group: group::activation -default_enabled: false diff --git a/config/routes.rb b/config/routes.rb index ebb0984a008..9c8ad8fe047 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -67,10 +67,7 @@ InitializerConnections.raise_if_new_database_connection do Gitlab.ee do resource :company, only: [:new, :create], controller: 'company' resources :groups_projects, only: [:new, :create] do - collection do - post :import - put :exit - end + post :import, on: :collection end draw :verification end diff --git a/db/docs/namespace_statistics.yml b/db/docs/namespace_statistics.yml index e84d5d563f8..4c294db3315 100644 --- a/db/docs/namespace_statistics.yml +++ b/db/docs/namespace_statistics.yml @@ -4,7 +4,8 @@ classes: - NamespaceStatistics feature_categories: - application_instrumentation -description: TODO -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/48d8bdca0493056a717cd7d9fee2e8b51d6b0502 +- consumables_cost_management +description: Stores usage statistics for both CI minutes and a limited set of storage types for a given namespace. This should not be confused with namespace_root_storage_statistics table which holds statistics across more storage types for a group. +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/965 milestone: '9.0' gitlab_schema: gitlab_main diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 78a9a8a9e67..d6fc9e60020 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -12027,6 +12027,7 @@ CI/CD variables for a GitLab instance. | <a id="cijobdownstreampipeline"></a>`downstreamPipeline` | [`Pipeline`](#pipeline) | Downstream pipeline for a bridge. | | <a id="cijobduration"></a>`duration` | [`Int`](#int) | Duration of the job in seconds. | | <a id="cijoberasedat"></a>`erasedAt` | [`Time`](#time) | When the job was erased. | +| <a id="cijobfailuremessage"></a>`failureMessage` | [`String`](#string) | Message on why the job failed. | | <a id="cijobfinishedat"></a>`finishedAt` | [`Time`](#time) | When a job has finished running. | | <a id="cijobid"></a>`id` | [`JobID`](#jobid) | ID of the job. | | <a id="cijobkind"></a>`kind` | [`CiJobKind!`](#cijobkind) | Indicates the type of job. | @@ -23847,6 +23848,7 @@ User permission on groups. | Value | Description | | ----- | ----------- | | <a id="grouppermissioncreate_projects"></a>`CREATE_PROJECTS` | Groups where the user can create projects. | +| <a id="grouppermissionimport_projects"></a>`IMPORT_PROJECTS` | Groups where the user can import projects to. | | <a id="grouppermissiontransfer_projects"></a>`TRANSFER_PROJECTS` | Groups where the user can transfer projects to. | ### `GroupReleaseSort` diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2936d4d21b9..196f692d6f6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -33134,6 +33134,9 @@ msgstr "" msgid "Please wait a moment, this page will automatically refresh when ready." msgstr "" +msgid "Please wait for the current action to complete" +msgstr "" + msgid "Please wait while we connect to your repository. Refresh at will." msgstr "" @@ -45497,9 +45500,6 @@ msgstr "" msgid "This namespace has already been taken! Please choose another one." msgstr "" -msgid "This namespace has already been taken. Choose a different one." -msgstr "" - msgid "This only applies to repository indexing operations." msgstr "" @@ -46609,7 +46609,7 @@ msgstr "" msgid "TransferGroup|Group contains contacts/organizations and you don't have enough permissions to move them to the new root group." msgstr "" -msgid "TransferGroup|Group contains projects with NPM packages." +msgid "TransferGroup|Group contains projects with NPM packages scoped to the current root level group." msgstr "" msgid "TransferGroup|Group is already a root group." @@ -46645,7 +46645,7 @@ msgstr "" msgid "TransferProject|Project with same name or path in target namespace already exists" msgstr "" -msgid "TransferProject|Root namespace can't be updated if project has NPM packages" +msgid "TransferProject|Root namespace can't be updated if the project has NPM packages scoped to the current root level namespace." msgstr "" msgid "TransferProject|You don't have permission to transfer projects into that namespace." @@ -50553,6 +50553,9 @@ msgstr "" msgid "You are not allowed to download code from this project." msgstr "" +msgid "You are not allowed to import projects in this namespace." +msgstr "" + msgid "You are not allowed to log in using password" msgstr "" @@ -50868,7 +50871,7 @@ msgstr "" msgid "You don't have permission to view this epic" msgstr "" -msgid "You don't have permissions to create this project" +msgid "You don't have permissions to import this project" msgstr "" msgid "You don't have sufficient permission to perform this action." diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb index ad8d63ec856..ccc5e04d672 100644 --- a/qa/qa/support/page/logging.rb +++ b/qa/qa/support/page/logging.rb @@ -36,9 +36,7 @@ module QA def find_element(name, **kwargs) log("finding :#{name} with args #{kwargs}") - - element = super - + element = log_slow_code(name, **kwargs) { super } log("found :#{name}") element @@ -46,9 +44,7 @@ module QA def all_elements(name, **kwargs) log("finding all :#{name} with args #{kwargs}") - - elements = super - + elements = log_slow_code(name, **kwargs) { super } log("found #{elements.size} :#{name}") if elements elements @@ -88,8 +84,7 @@ module QA log(msg.join(' '), :info) log("with args #{kwargs}") - - super + log_slow_code(name, **kwargs) { super } end def act_via_capybara(method, locator, **kwargs) @@ -113,32 +108,28 @@ module QA end def has_element?(name, **kwargs) - found = super - + found = log_slow_code(name, **kwargs) { super } log_has_element_or_not('has_element?', name, found, **kwargs) found end def has_no_element?(name, **kwargs) - found = super - + found = log_slow_code(name, **kwargs) { super } log_has_element_or_not('has_no_element?', name, found, **kwargs) found end def has_text?(text, **kwargs) - found = super - + found = log_slow_code(text, **kwargs) { super } log(%(has_text?('#{text}', wait: #{kwargs[:wait] || Capybara.default_max_wait_time}) returned #{found})) found end def has_no_text?(text, **kwargs) - found = super - + found = log_slow_code(text, **kwargs) { super } log(%(has_no_text?('#{text}', wait: #{kwargs[:wait] || Capybara.default_max_wait_time}) returned #{found})) found @@ -146,13 +137,11 @@ module QA def finished_loading?(wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) log('waiting for loading to complete...') - now = Time.now - - loaded = super - - log("loading complete after #{Time.now - now} seconds") + log_slow_code { super } + end - loaded + def wait_for_requests(skip_finished_loading_check: false, skip_resp_code_check: false) + log_slow_code { super } end def wait_for_animated_element(name) @@ -209,6 +198,21 @@ module QA log(msg.compact.join(' ')) end + + # Prints warning log if code duration is slower than threshold + # @param [String (frozen)] paramInfo is info relating to the slow element + def log_slow_code(param_info = '', **kwargs) + starting = kwargs.fetch(:starting_time, Time.now) + result = yield + ending = kwargs.fetch(:ending_time, Time.now) + duration = (ending - starting).round(3) + if duration > kwargs.fetch(:log_slow_threshold, 0.5) + caller_method_name = caller_locations(1, 1).first.label + QA::Runtime::Logger.warn("Potentially Slow Code '#{caller_method_name} #{param_info}' took #{duration}s") + end + + result + end end end end diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb index 1a82cda2585..646b77a7f84 100644 --- a/qa/spec/page/logging_spec.rb +++ b/qa/spec/page/logging_spec.rb @@ -62,6 +62,20 @@ RSpec.describe QA::Support::Page::Logging do .to output(/finding :element with args {:class=>"active"}/).to_stdout_from_any_process end + it 'logs a warning if find_element is slow' do + starting = Time.now + ending = starting + 1.4 + expected_msg = /Potentially Slow Code 'find_element element' took 1.4s/ + + # verify logs a warning message to indicate potentially slow code lookups + expect { subject.find_element(:element, starting_time: starting, ending_time: ending) } + .to output(expected_msg).to_stdout_from_any_process + + # verify it doesn't log a warning message if within allowed limits + expect { subject.find_element(:element, starting_time: starting, ending_time: ending, log_slow_threshold: 1.5) } + .not_to output(expected_msg).to_stdout_from_any_process + end + it 'logs click_element' do expect { subject.click_element(:element) } .to output(/clicking :element/).to_stdout_from_any_process @@ -127,8 +141,6 @@ RSpec.describe QA::Support::Page::Logging do it 'logs finished_loading?' do expect { subject.finished_loading? } .to output(/waiting for loading to complete\.\.\./).to_stdout_from_any_process - expect { subject.finished_loading? } - .to output(/loading complete after .* seconds$/).to_stdout_from_any_process end it 'logs within_element' do diff --git a/scripts/gitlab_component_helpers.sh b/scripts/gitlab_component_helpers.sh index 301d4fb5d37..3d5116d6cc2 100644 --- a/scripts/gitlab_component_helpers.sh +++ b/scripts/gitlab_component_helpers.sh @@ -180,6 +180,32 @@ function check_fixtures_download() { fi } +function check_fixtures_reuse() { + if [[ "${REUSE_FRONTEND_FIXTURES_ENABLED:-}" != "true" ]]; then + rm -rf "tmp/tests/frontend"; + return 1 + fi + + # Note: Currently, reusing frontend fixtures is only supported in EE. + # Other projects will be supported through this issue in the future: https://gitlab.com/gitlab-org/gitlab/-/issues/393615. + if [[ "${CI_PROJECT_NAME}" != "gitlab" ]] || [[ "${CI_JOB_NAME}" =~ "foss" ]]; then + rm -rf "tmp/tests/frontend"; + return 1 + fi + + if [[ -d "tmp/tests/frontend" ]]; then + # Remove tmp/tests/frontend/ except on the first parallelized job so that depending + # jobs don't download the exact same artifact multiple times. + if [[ -n "${CI_NODE_INDEX:-}" ]] && [[ "${CI_NODE_INDEX}" -ne 1 ]]; then + echoinfo "INFO: Removing 'tmp/tests/frontend' as we're on node ${CI_NODE_INDEX}."; + rm -rf "tmp/tests/frontend"; + fi + return 0 + else + return 1 + fi +} + function create_fixtures_package() { create_package "${FIXTURES_PACKAGE}" "${FIXTURES_PATH}" } diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 055c98ebdbc..906cc5cb336 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Import::BitbucketController do +RSpec.describe Import::BitbucketController, feature_category: :importers do include ImportSpecHelper let(:user) { create(:user) } @@ -445,5 +445,16 @@ RSpec.describe Import::BitbucketController do ) end end + + context 'when user can not import projects' do + let!(:other_namespace) { create(:group, name: 'other_namespace').tap { |other_namespace| other_namespace.add_developer(user) } } + + it 'returns 422 response' do + post :create, params: { target_namespace: other_namespace.name }, format: :json + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(response.parsed_body['errors']).to eq('You are not allowed to import projects in this namespace.') + end + end end end diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb index ac56d3af54f..b2a56423253 100644 --- a/spec/controllers/import/bitbucket_server_controller_spec.rb +++ b/spec/controllers/import/bitbucket_server_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Import::BitbucketServerController do +RSpec.describe Import::BitbucketServerController, feature_category: :importers do let(:user) { create(:user) } let(:project_key) { 'test-project' } let(:repo_slug) { 'some-repo' } diff --git a/spec/controllers/import/fogbugz_controller_spec.rb b/spec/controllers/import/fogbugz_controller_spec.rb index e2d59fc213a..40a5c59fa2d 100644 --- a/spec/controllers/import/fogbugz_controller_spec.rb +++ b/spec/controllers/import/fogbugz_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Import::FogbugzController do +RSpec.describe Import::FogbugzController, feature_category: :importers do include ImportSpecHelper let(:user) { create(:user) } diff --git a/spec/controllers/import/gitea_controller_spec.rb b/spec/controllers/import/gitea_controller_spec.rb index 568712d29cb..7466ffb2393 100644 --- a/spec/controllers/import/gitea_controller_spec.rb +++ b/spec/controllers/import/gitea_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Import::GiteaController do +RSpec.describe Import::GiteaController, feature_category: :importers do include ImportSpecHelper let(:provider) { :gitea } diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb index 7b3978297fb..2c09f8c010e 100644 --- a/spec/controllers/import/gitlab_controller_spec.rb +++ b/spec/controllers/import/gitlab_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Import::GitlabController do +RSpec.describe Import::GitlabController, feature_category: :importers do include ImportSpecHelper let(:user) { create(:user) } diff --git a/spec/controllers/import/manifest_controller_spec.rb b/spec/controllers/import/manifest_controller_spec.rb index 6f805b44e89..23d5d37ed88 100644 --- a/spec/controllers/import/manifest_controller_spec.rb +++ b/spec/controllers/import/manifest_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Import::ManifestController, :clean_gitlab_redis_shared_state do +RSpec.describe Import::ManifestController, :clean_gitlab_redis_shared_state, feature_category: :importers do include ImportSpecHelper let_it_be(:user) { create(:user) } @@ -45,7 +45,7 @@ RSpec.describe Import::ManifestController, :clean_gitlab_redis_shared_state do end end - context 'when the user cannot create projects in the group' do + context 'when the user cannot import projects in the group' do it 'displays an error' do sign_in(create(:user)) diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb index b4704d56cd9..4502f3d7bd9 100644 --- a/spec/controllers/projects/imports_controller_spec.rb +++ b/spec/controllers/projects/imports_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::ImportsController do +RSpec.describe Projects::ImportsController, feature_category: :importers do let(:user) { create(:user) } let(:project) { create(:project) } @@ -149,17 +149,7 @@ RSpec.describe Projects::ImportsController do import_state.update!(status: :started) end - context 'when group allows developers to create projects' do - let(:group) { create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) } - - it 'renders template' do - get :show, params: { namespace_id: project.namespace.to_param, project_id: project } - - expect(response).to render_template :show - end - end - - context 'when group prohibits developers to create projects' do + context 'when group prohibits developers to import projects' do let(:group) { create(:group, project_creation_level: Gitlab::Access::MAINTAINER_PROJECT_ACCESS) } it 'returns 404 response' do diff --git a/spec/finders/groups/accepting_project_imports_finder_spec.rb b/spec/finders/groups/accepting_project_imports_finder_spec.rb new file mode 100644 index 00000000000..4e06c2cbc67 --- /dev/null +++ b/spec/finders/groups/accepting_project_imports_finder_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Groups::AcceptingProjectImportsFinder, feature_category: :importers do + let_it_be(:user) { create(:user) } + let_it_be(:group_where_direct_owner) { create(:group) } + let_it_be(:subgroup_of_group_where_direct_owner) { create(:group, parent: group_where_direct_owner) } + let_it_be(:group_where_direct_maintainer) { create(:group) } + let_it_be(:group_where_direct_maintainer_but_cant_create_projects) do + create(:group, project_creation_level: Gitlab::Access::NO_ONE_PROJECT_ACCESS) + end + + let_it_be(:group_where_direct_developer_but_developers_cannot_create_projects) { create(:group) } + let_it_be(:group_where_direct_developer) do + create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) + end + + let_it_be(:shared_with_group_where_direct_owner_as_owner) { create(:group) } + + let_it_be(:shared_with_group_where_direct_owner_as_developer) do + create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) + end + + let_it_be(:shared_with_group_where_direct_owner_as_developer_but_developers_cannot_create_projects) do + create(:group) + end + + let_it_be(:shared_with_group_where_direct_developer_as_maintainer) do + create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) + end + + let_it_be(:shared_with_group_where_direct_owner_as_guest) { create(:group) } + let_it_be(:shared_with_group_where_direct_owner_as_maintainer) { create(:group) } + let_it_be(:shared_with_group_where_direct_developer_as_owner) do + create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) + end + + let_it_be(:subgroup_of_shared_with_group_where_direct_owner_as_maintainer) do + create(:group, parent: shared_with_group_where_direct_owner_as_maintainer) + end + + before do + group_where_direct_owner.add_owner(user) + group_where_direct_maintainer.add_maintainer(user) + group_where_direct_developer_but_developers_cannot_create_projects.add_developer(user) + group_where_direct_developer.add_developer(user) + + create(:group_group_link, :owner, + shared_with_group: group_where_direct_owner, + shared_group: shared_with_group_where_direct_owner_as_owner + ) + + create(:group_group_link, :developer, + shared_with_group: group_where_direct_owner, + shared_group: shared_with_group_where_direct_owner_as_developer_but_developers_cannot_create_projects + ) + + create(:group_group_link, :maintainer, + shared_with_group: group_where_direct_developer, + shared_group: shared_with_group_where_direct_developer_as_maintainer + ) + + create(:group_group_link, :developer, + shared_with_group: group_where_direct_owner, + shared_group: shared_with_group_where_direct_owner_as_developer + ) + + create(:group_group_link, :guest, + shared_with_group: group_where_direct_owner, + shared_group: shared_with_group_where_direct_owner_as_guest + ) + + create(:group_group_link, :maintainer, + shared_with_group: group_where_direct_owner, + shared_group: shared_with_group_where_direct_owner_as_maintainer + ) + + create(:group_group_link, :owner, + shared_with_group: group_where_direct_developer_but_developers_cannot_create_projects, + shared_group: shared_with_group_where_direct_developer_as_owner + ) + end + + describe '#execute' do + subject(:result) { described_class.new(user).execute } + + it 'only returns groups where the user has access to import projects' do + expect(result).to match_array([ + group_where_direct_owner, + subgroup_of_group_where_direct_owner, + group_where_direct_maintainer, + # groups arising from group shares + shared_with_group_where_direct_owner_as_owner, + shared_with_group_where_direct_owner_as_maintainer, + subgroup_of_shared_with_group_where_direct_owner_as_maintainer + ]) + + expect(result).not_to include(group_where_direct_developer) + expect(result).not_to include(shared_with_group_where_direct_developer_as_owner) + expect(result).not_to include(shared_with_group_where_direct_developer_as_maintainer) + expect(result).not_to include(shared_with_group_where_direct_owner_as_developer) + end + end +end diff --git a/spec/finders/groups/user_groups_finder_spec.rb b/spec/finders/groups/user_groups_finder_spec.rb index 999079468e5..f6df396037c 100644 --- a/spec/finders/groups/user_groups_finder_spec.rb +++ b/spec/finders/groups/user_groups_finder_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Groups::UserGroupsFinder do +RSpec.describe Groups::UserGroupsFinder, feature_category: :subgroups do describe '#execute' do let_it_be(:user) { create(:user) } let_it_be(:root_group) { create(:group, name: 'Root group', path: 'root-group') } @@ -98,6 +98,24 @@ RSpec.describe Groups::UserGroupsFinder do end end + context 'when permission is :import_projects' do + let(:arguments) { { permission_scope: :import_projects } } + + specify do + is_expected.to contain_exactly( + public_maintainer_group, + public_owner_group, + private_maintainer_group + ) + end + + it_behaves_like 'user group finder searching by name or path' do + let(:keyword_search_expected_groups) do + [public_maintainer_group] + end + end + end + context 'when permission is :transfer_projects' do let(:arguments) { { permission_scope: :transfer_projects } } diff --git a/spec/frontend/import_entities/components/group_dropdown_spec.js b/spec/frontend/import_entities/components/group_dropdown_spec.js index b44bc33de6f..14f39a35387 100644 --- a/spec/frontend/import_entities/components/group_dropdown_spec.js +++ b/spec/frontend/import_entities/components/group_dropdown_spec.js @@ -6,7 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import GroupDropdown from '~/import_entities/components/group_dropdown.vue'; import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; -import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql'; +import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql'; Vue.use(VueApollo); @@ -49,7 +49,7 @@ describe('Import entities group dropdown component', () => { const createComponent = (propsData) => { const apolloProvider = createMockApollo([ - [searchNamespacesWhereUserCanCreateProjectsQuery, () => SEARCH_NAMESPACES_MOCK], + [searchNamespacesWhereUserCanImportProjectsQuery, () => SEARCH_NAMESPACES_MOCK], ]); namespacesTracker = jest.fn(); diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js index b1aa94cf418..dae5671777c 100644 --- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js @@ -15,7 +15,7 @@ import ImportTable from '~/import_entities/import_groups/components/import_table import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql'; import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; -import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql'; +import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql'; import { AVAILABLE_NAMESPACES, @@ -74,7 +74,7 @@ describe('import table', () => { apolloProvider = createMockApollo( [ [ - searchNamespacesWhereUserCanCreateProjectsQuery, + searchNamespacesWhereUserCanImportProjectsQuery, () => Promise.resolve(availableNamespacesFixture), ], ], diff --git a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js index a524d9ebdb0..a957e85723f 100644 --- a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js @@ -8,7 +8,7 @@ import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue import { STATUSES } from '~/import_entities/constants'; import ImportTargetCell from '~/import_entities/import_groups/components/import_target_cell.vue'; import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; -import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql'; +import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql'; import { generateFakeEntry, @@ -42,7 +42,7 @@ describe('import target cell', () => { const createComponent = (props) => { apolloProvider = createMockApollo([ [ - searchNamespacesWhereUserCanCreateProjectsQuery, + searchNamespacesWhereUserCanImportProjectsQuery, () => Promise.resolve(availableNamespacesFixture), ], ]); diff --git a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js index 53d69b01d97..d4b581c3fcf 100644 --- a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js +++ b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js @@ -3,6 +3,7 @@ import { mount, shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; +import { s__ } from '~/locale'; import waitForPromises from 'helpers/wait_for_promises'; import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue'; import JobsSkeletonLoader from '~/pages/admin/jobs/components/jobs_skeleton_loader.vue'; @@ -11,16 +12,22 @@ import getCancelableJobsQuery from '~/pages/admin/jobs/components/table/graphql/ import AdminJobsTableApp from '~/pages/admin/jobs/components/table/admin_jobs_table_app.vue'; import CancelJobs from '~/pages/admin/jobs/components/cancel_jobs.vue'; import JobsTable from '~/jobs/components/table/jobs_table.vue'; - +import { createAlert } from '~/alert'; +import { TEST_HOST } from 'spec/test_constants'; +import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue'; +import * as urlUtils from '~/lib/utils/url_utility'; import { mockAllJobsResponsePaginated, mockCancelableJobsCountResponse, mockAllJobsResponseEmpty, statuses, + mockFailedSearchToken, } from '../../../../../jobs/mock_data'; Vue.use(VueApollo); +jest.mock('~/alert'); + describe('Job table app', () => { let wrapper; @@ -36,6 +43,7 @@ describe('Job table app', () => { const findAlert = () => wrapper.findComponent(GlAlert); const findTabs = () => wrapper.findComponent(JobsTableTabs); const findCancelJobsButton = () => wrapper.findComponent(CancelJobs); + const findFilteredSearch = () => wrapper.findComponent(JobsFilteredSearch); const triggerInfiniteScroll = () => wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear'); @@ -186,4 +194,100 @@ describe('Job table app', () => { expect(findCancelJobsButton().exists()).toBe(false); }); }); + + describe('filtered search', () => { + it('should display filtered search', () => { + createComponent(); + + expect(findFilteredSearch().exists()).toBe(true); + }); + + // this test should be updated once BE supports tab and filtered search filtering + // https://gitlab.com/gitlab-org/gitlab/-/issues/356210 + it.each` + scope | shouldDisplay + ${null} | ${true} + ${['FAILED', 'SUCCESS', 'CANCELED']} | ${false} + `( + 'with tab scope $scope the filtered search displays $shouldDisplay', + async ({ scope, shouldDisplay }) => { + createComponent(); + + await waitForPromises(); + + await findTabs().vm.$emit('fetchJobsByStatus', scope); + + expect(findFilteredSearch().exists()).toBe(shouldDisplay); + }, + ); + + it('refetches jobs query when filtering', async () => { + createComponent(); + + jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn()); + + expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0); + + await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]); + + expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1); + }); + + it('shows raw text warning when user inputs raw text', async () => { + const expectedWarning = { + message: s__( + 'Jobs|Raw text search is not currently supported for the jobs filtered search feature. Please use the available search tokens.', + ), + type: 'warning', + }; + + createComponent(); + + jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn()); + + await findFilteredSearch().vm.$emit('filterJobsBySearch', ['raw text']); + + expect(createAlert).toHaveBeenCalledWith(expectedWarning); + expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0); + }); + + it('updates URL query string when filtering jobs by status', async () => { + createComponent(); + + jest.spyOn(urlUtils, 'updateHistory'); + + await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]); + + expect(urlUtils.updateHistory).toHaveBeenCalledWith({ + url: `${TEST_HOST}/?statuses=FAILED`, + }); + }); + + it('resets query param after clearing tokens', () => { + createComponent(); + + jest.spyOn(urlUtils, 'updateHistory'); + + findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]); + + expect(successHandler).toHaveBeenCalledWith({ + first: 50, + statuses: 'FAILED', + }); + expect(urlUtils.updateHistory).toHaveBeenCalledWith({ + url: `${TEST_HOST}/?statuses=FAILED`, + }); + + findFilteredSearch().vm.$emit('filterJobsBySearch', []); + + expect(urlUtils.updateHistory).toHaveBeenCalledWith({ + url: `${TEST_HOST}/`, + }); + + expect(successHandler).toHaveBeenCalledWith({ + first: 50, + statuses: null, + }); + }); + }); }); diff --git a/spec/frontend/projects/new/components/app_spec.js b/spec/frontend/projects/new/components/app_spec.js index 16576523c66..60d8385eb91 100644 --- a/spec/frontend/projects/new/components/app_spec.js +++ b/spec/frontend/projects/new/components/app_spec.js @@ -41,6 +41,22 @@ describe('Experimental new project creation app', () => { ).toBe(isCiCdAvailable); }); + it.each` + canImportProjects | outcome + ${false} | ${'do not show Import panel'} + ${true} | ${'show Import panel'} + `('$outcome when canImportProjects is $canImportProjects', ({ canImportProjects }) => { + createComponent({ + canImportProjects, + }); + + expect( + findNewNamespacePage() + .props() + .panels.some((p) => p.name === 'import_project'), + ).toBe(canImportProjects); + }); + it('creates correct breadcrumbs for top-level projects', () => { createComponent(); diff --git a/spec/frontend/vue_shared/alert_details/router_spec.js b/spec/frontend/vue_shared/alert_details/router_spec.js deleted file mode 100644 index e3efc104862..00000000000 --- a/spec/frontend/vue_shared/alert_details/router_spec.js +++ /dev/null @@ -1,35 +0,0 @@ -import createRouter from '~/vue_shared/alert_details/router'; -import setWindowLocation from 'helpers/set_window_location_helper'; - -const BASE_PATH = '/-/alert_management/1/details'; -const EMPTY_HASH = ''; -const NOOP = () => {}; - -describe('AlertDetails router', () => { - const originalLocation = window.location.href; - let router; - - beforeEach(() => { - setWindowLocation(originalLocation); - router = createRouter(BASE_PATH); - }); - - describe('redirects hash route mode URLs to history route mode', () => { - it.each` - hashPath | historyPath - ${'/#/overview'} | ${'/overview'} - ${'#/overview'} | ${'/overview'} - ${'/#/'} | ${'/'} - ${'#/'} | ${'/'} - ${'/#'} | ${'/'} - ${'#'} | ${'/'} - ${'/'} | ${'/'} - ${'/overview'} | ${'/overview'} - `('should redirect "$hashPath" to "$historyPath"', ({ hashPath, historyPath }) => { - router.push(hashPath, NOOP); - - expect(window.location.hash).toBe(EMPTY_HASH); - expect(window.location.pathname).toBe(BASE_PATH + historyPath); - }); - }); -}); diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb index 7715ccdd075..444e941d7a6 100644 --- a/spec/graphql/types/ci/job_type_spec.rb +++ b/spec/graphql/types/ci/job_type_spec.rb @@ -56,6 +56,7 @@ RSpec.describe Types::Ci::JobType, feature_category: :continuous_integration do canPlayJob scheduled trace + failure_message ] expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/support_specs/helpers/packages/npm_spec.rb b/spec/lib/api/helpers/packages/npm_spec.rb index e1316a10fb1..e1316a10fb1 100644 --- a/spec/support_specs/helpers/packages/npm_spec.rb +++ b/spec/lib/api/helpers/packages/npm_spec.rb diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 92849efc462..2128e70e432 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -882,46 +882,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do end end - describe '#has_packages?' do - let_it_be(:project) { create(:project, :public) } - - subject { project.has_packages?(package_type) } - - shared_examples 'returning true examples' do - let!(:package) { create("#{package_type}_package", project: project) } - - it { is_expected.to be true } - end - - shared_examples 'returning false examples' do - it { is_expected.to be false } - end - - context 'with maven packages' do - it_behaves_like 'returning true examples' do - let(:package_type) { :maven } - end - end - - context 'with npm packages' do - it_behaves_like 'returning true examples' do - let(:package_type) { :npm } - end - end - - context 'with conan packages' do - it_behaves_like 'returning true examples' do - let(:package_type) { :conan } - end - end - - context 'with no package type' do - it_behaves_like 'returning false examples' do - let(:package_type) { nil } - end - end - end - describe '#ci_pipelines' do let_it_be(:project) { create(:project) } @@ -7660,48 +7620,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do end end - describe '#has_packages?' do - let(:project) { create(:project, :public) } - - subject { project.has_packages?(package_type) } - - shared_examples 'has_package' do - context 'package of package_type exists' do - let!(:package) { create("#{package_type}_package", project: project) } - - it { is_expected.to be true } - end - - context 'package of package_type does not exist' do - it { is_expected.to be false } - end - end - - context 'with maven packages' do - it_behaves_like 'has_package' do - let(:package_type) { :maven } - end - end - - context 'with npm packages' do - it_behaves_like 'has_package' do - let(:package_type) { :npm } - end - end - - context 'with conan packages' do - it_behaves_like 'has_package' do - let(:package_type) { :conan } - end - end - - context 'calling has_package? with nil' do - let(:package_type) { nil } - - it { is_expected.to be false } - end - end - describe 'with Debian Distributions' do subject { create(:project) } @@ -7822,6 +7740,29 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do end end + describe '#has_namespaced_npm_packages?' do + let_it_be(:namespace) { create(:namespace, path: 'test') } + let_it_be(:project) { create(:project, :public, namespace: namespace) } + + subject { project.has_namespaced_npm_packages? } + + context 'with scope of the namespace path' do + let_it_be(:package) { create(:npm_package, project: project, name: "@#{namespace.path}/foo") } + + it { is_expected.to be true } + end + + context 'without scope of the namespace path' do + let_it_be(:package) { create(:npm_package, project: project, name: "@someotherscope/foo") } + + it { is_expected.to be false } + end + + context 'without packages' do + it { is_expected.to be false } + end + end + describe '#package_already_taken?' do let_it_be(:namespace) { create(:namespace, path: 'test') } let_it_be(:project) { create(:project, :public, namespace: namespace) } diff --git a/spec/policies/namespaces/user_namespace_policy_spec.rb b/spec/policies/namespaces/user_namespace_policy_spec.rb index bb821490e30..3488f33f15c 100644 --- a/spec/policies/namespaces/user_namespace_policy_spec.rb +++ b/spec/policies/namespaces/user_namespace_policy_spec.rb @@ -2,13 +2,13 @@ require 'spec_helper' -RSpec.describe Namespaces::UserNamespacePolicy do +RSpec.describe Namespaces::UserNamespacePolicy, feature_category: :subgroups do let_it_be(:user) { create(:user) } let_it_be(:owner) { create(:user) } let_it_be(:admin) { create(:admin) } let_it_be(:namespace) { create(:user_namespace, owner: owner) } - let(:owner_permissions) { [:owner_access, :create_projects, :admin_namespace, :read_namespace, :read_statistics, :transfer_projects, :admin_package, :read_billing, :edit_billing] } + let(:owner_permissions) { [:owner_access, :create_projects, :admin_namespace, :read_namespace, :read_statistics, :transfer_projects, :admin_package, :read_billing, :edit_billing, :import_projects] } subject { described_class.new(current_user, namespace) } @@ -34,6 +34,7 @@ RSpec.describe Namespaces::UserNamespacePolicy do it { is_expected.to be_disallowed(:create_projects) } it { is_expected.to be_disallowed(:transfer_projects) } + it { is_expected.to be_disallowed(:import_projects) } end context 'bot user' do @@ -41,6 +42,7 @@ RSpec.describe Namespaces::UserNamespacePolicy do it { is_expected.to be_disallowed(:create_projects) } it { is_expected.to be_disallowed(:transfer_projects) } + it { is_expected.to be_disallowed(:import_projects) } end end @@ -103,4 +105,26 @@ RSpec.describe Namespaces::UserNamespacePolicy do it { is_expected.to be_disallowed(:create_projects) } end end + + describe 'import projects' do + context 'when user can import projects' do + let(:current_user) { owner } + + before do + allow(current_user).to receive(:can_import_project?).and_return(true) + end + + it { is_expected.to be_allowed(:import_projects) } + end + + context 'when user cannot create projects' do + let(:current_user) { user } + + before do + allow(current_user).to receive(:can_import_project?).and_return(false) + end + + it { is_expected.to be_disallowed(:import_projects) } + end + end end diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb index 6bf36a52419..58d19ae2332 100644 --- a/spec/presenters/ci/build_presenter_spec.rb +++ b/spec/presenters/ci/build_presenter_spec.rb @@ -174,6 +174,22 @@ RSpec.describe Ci::BuildPresenter do end end + describe '#failure_message' do + let_it_be(:build) { create(:ci_build, :failed, failure_reason: 2) } + + it 'returns a verbose failure message' do + expect(subject.failure_message).to eq('There has been an API failure, please try again') + end + + context 'when the build has not failed' do + let_it_be(:build) { create(:ci_build, :success, failure_reason: 2) } + + it 'does not return any failure message' do + expect(subject.failure_message).to be_nil + end + end + end + describe '#callout_failure_message' do let(:build) { create(:ci_build, :failed, :api_failure) } diff --git a/spec/requests/api/graphql/ci/job_spec.rb b/spec/requests/api/graphql/ci/job_spec.rb index 8121c5e5c85..960697db239 100644 --- a/spec/requests/api/graphql/ci/job_spec.rb +++ b/spec/requests/api/graphql/ci/job_spec.rb @@ -52,7 +52,8 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)', feature_category: :c 'duration' => 25, 'kind' => 'BUILD', 'queuedDuration' => 2.0, - 'status' => job_2.status.upcase + 'status' => job_2.status.upcase, + 'failureMessage' => job_2.present.failure_message ) end diff --git a/spec/requests/import/gitlab_projects_controller_spec.rb b/spec/requests/import/gitlab_projects_controller_spec.rb index b2c2d306e53..fe3ea9e9c9e 100644 --- a/spec/requests/import/gitlab_projects_controller_spec.rb +++ b/spec/requests/import/gitlab_projects_controller_spec.rb @@ -90,4 +90,16 @@ RSpec.describe Import::GitlabProjectsController, feature_category: :importers do subject { post authorize_import_gitlab_project_path, headers: workhorse_headers } end end + + describe 'GET new' do + context 'when the user is not allowed to import projects' do + let!(:group) { create(:group).tap { |group| group.add_developer(user) } } + + it 'returns 404' do + get new_import_gitlab_project_path, params: { namespace_id: group.id } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end end diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb index 475cd250e7c..d6eb060ea7e 100644 --- a/spec/services/groups/transfer_service_spec.rb +++ b/spec/services/groups/transfer_service_spec.rb @@ -35,10 +35,10 @@ RSpec.describe Groups::TransferService, :sidekiq_inline, feature_category: :subg end context 'handling packages' do - let_it_be(:group) { create(:group, :public) } - let_it_be(:new_group) { create(:group, :public) } + let_it_be(:group) { create(:group) } + let_it_be(:new_group) { create(:group) } - let(:project) { create(:project, :public, namespace: group) } + let_it_be(:project) { create(:project, namespace: group) } before do group.add_owner(user) @@ -46,46 +46,63 @@ RSpec.describe Groups::TransferService, :sidekiq_inline, feature_category: :subg end context 'with an npm package' do - before do - create(:npm_package, project: project) - end + let_it_be(:npm_package) { create(:npm_package, project: project, name: "@testscope/test") } - shared_examples 'transfer not allowed' do - it 'does not allow transfer when there is a root namespace change' do + shared_examples 'transfer allowed' do + it 'allows transfer' do transfer_service.execute(new_group) - expect(transfer_service.error).to eq('Transfer failed: Group contains projects with NPM packages.') - expect(group.parent).not_to eq(new_group) + expect(transfer_service.error).to be nil + expect(group.parent).to eq(new_group) end end - it_behaves_like 'transfer not allowed' + it_behaves_like 'transfer allowed' context 'with a project within subgroup' do let_it_be(:root_group) { create(:group) } let_it_be(:group) { create(:group, parent: root_group) } + let_it_be(:project) { create(:project, namespace: group) } before do root_group.add_owner(user) end - it_behaves_like 'transfer not allowed' + it_behaves_like 'transfer allowed' context 'without a root namespace change' do - let(:new_group) { create(:group, parent: root_group) } + let_it_be(:new_group) { create(:group, parent: root_group) } + + it_behaves_like 'transfer allowed' + end + + context 'with namespaced packages present' do + let_it_be(:package) { create(:npm_package, project: project, name: "@#{project.root_namespace.path}/test") } - it 'allows transfer' do + it 'does not allow transfer' do transfer_service.execute(new_group) - expect(transfer_service.error).to be nil - expect(group.parent).to eq(new_group) + expect(transfer_service.error).to eq('Transfer failed: Group contains projects with NPM packages scoped to the current root level group.') + expect(group.parent).not_to eq(new_group) + end + + context 'namespaced package is pending destruction' do + let!(:group) { create(:group) } + + before do + package.pending_destruction! + end + + it_behaves_like 'transfer allowed' end end context 'when transferring a group into a root group' do - let(:new_group) { nil } + let_it_be(:root_group) { create(:group) } + let_it_be(:group) { create(:group, parent: root_group) } + let_it_be(:new_group) { nil } - it_behaves_like 'transfer not allowed' + it_behaves_like 'transfer allowed' end end end diff --git a/spec/services/import/bitbucket_server_service_spec.rb b/spec/services/import/bitbucket_server_service_spec.rb index aea6c45b3a8..ca554fb01c3 100644 --- a/spec/services/import/bitbucket_server_service_spec.rb +++ b/spec/services/import/bitbucket_server_service_spec.rb @@ -93,7 +93,7 @@ RSpec.describe Import::BitbucketServerService, feature_category: :importers do result = subject.execute(credentials) expect(result).to include( - message: "You don't have permissions to create this project", + message: "You don't have permissions to import this project", status: :error, http_status: :unauthorized ) diff --git a/spec/services/import/fogbugz_service_spec.rb b/spec/services/import/fogbugz_service_spec.rb index 6953213add7..ad02dc31da1 100644 --- a/spec/services/import/fogbugz_service_spec.rb +++ b/spec/services/import/fogbugz_service_spec.rb @@ -61,7 +61,7 @@ RSpec.describe Import::FogbugzService, feature_category: :importers do result = subject.execute(credentials) expect(result).to include( - message: "You don't have permissions to create this project", + message: "You don't have permissions to import this project", status: :error, http_status: :unauthorized ) diff --git a/spec/services/import/github_service_spec.rb b/spec/services/import/github_service_spec.rb index 5d762568a62..a8928fb5c09 100644 --- a/spec/services/import/github_service_spec.rb +++ b/spec/services/import/github_service_spec.rb @@ -291,7 +291,7 @@ RSpec.describe Import::GithubService, feature_category: :importers do { status: :error, http_status: :unprocessable_entity, - message: 'This namespace has already been taken. Choose a different one.' + message: 'You are not allowed to import projects in this namespace.' } end end diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb index d21b11f8ecb..638004f2520 100644 --- a/spec/services/packages/npm/create_package_service_spec.rb +++ b/spec/services/packages/npm/create_package_service_spec.rb @@ -61,17 +61,90 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r end end - context 'with a too large metadata structure' do - before do - params[:versions][version][:test] = 'test' * 10000 + context 'when the npm metadatum creation results in a size error' do + shared_examples 'a package json structure size too large error' do + it 'does not create the package' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).with( + instance_of(ActiveRecord::RecordInvalid), + field_sizes: expected_field_sizes + ) + + expect { subject }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Package json structure is too large') + .and not_change { Packages::Package.count } + .and not_change { Packages::Package.npm.count } + .and not_change { Packages::Tag.count } + .and not_change { Packages::Npm::Metadatum.count } + end + end + + context 'when some of the field sizes are above the error tracking size' do + let(:package_json) do + params[:versions][version].except(*::Packages::Npm::CreatePackageService::PACKAGE_JSON_NOT_ALLOWED_FIELDS) + end + + # Only the fields that exceed the field size limit should be passed to error tracking + let(:expected_field_sizes) do + { + 'test' => ('test' * 10000).size, + 'field2' => ('a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING + 1)).size + } + end + + before do + params[:versions][version][:test] = 'test' * 10000 + params[:versions][version][:field1] = + 'a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING - 1) + params[:versions][version][:field2] = + 'a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING + 1) + end + + it_behaves_like 'a package json structure size too large error' + end + + context 'when all of the field sizes are below the error tracking size' do + let(:package_json) do + params[:versions][version].except(*::Packages::Npm::CreatePackageService::PACKAGE_JSON_NOT_ALLOWED_FIELDS) + end + + let(:expected_size) { ('a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING - 1)).size } + # Only the five largest fields should be passed to error tracking + let(:expected_field_sizes) do + { + 'field1' => expected_size, + 'field2' => expected_size, + 'field3' => expected_size, + 'field4' => expected_size, + 'field5' => expected_size + } + end + + before do + 5.times do |i| + params[:versions][version]["field#{i + 1}"] = + 'a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING - 1) + end + end + + it_behaves_like 'a package json structure size too large error' end + end + + context 'when the npm metadatum creation results in a different error' do + it 'does not track the error' do + error_message = 'boom' + invalid_npm_metadatum_error = ActiveRecord::RecordInvalid.new( + build(:npm_metadatum).tap do |metadatum| + metadatum.errors.add(:base, error_message) + end + ) + + allow_next_instance_of(::Packages::Package) do |package| + allow(package).to receive(:create_npm_metadatum!).and_raise(invalid_npm_metadatum_error) + end + + expect(Gitlab::ErrorTracking).not_to receive(:track_exception) - it 'does not create the package' do - expect { subject }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Package json structure is too large') - .and not_change { Packages::Package.count } - .and not_change { Packages::Package.npm.count } - .and not_change { Packages::Tag.count } - .and not_change { Packages::Npm::Metadatum.count } + expect { subject }.to raise_error(ActiveRecord::RecordInvalid, /#{error_message}/) end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 495e2277d43..35b715d82ee 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -254,6 +254,23 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :projects end it_behaves_like 'has sync-ed traversal_ids' + + context 'when project is an import' do + context 'when user is not allowed to import projects' do + let(:group) do + create(:group).tap do |group| + group.add_developer(user) + end + end + + it 'does not create the project' do + project = create_project(user, opts.merge!(namespace_id: group.id, import_type: 'gitlab_project')) + + expect(project).not_to be_persisted + expect(project.errors.messages[:user].first).to eq('is not allowed to import projects') + end + end + end end context 'group sharing', :sidekiq_inline do diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 92ed5ef3f0a..2704458ca4d 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -20,12 +20,32 @@ RSpec.describe Projects::TransferService, feature_category: :projects do subject(:transfer_service) { described_class.new(project, user) } - let!(:package) { create(:npm_package, project: project) } + let!(:package) { create(:npm_package, project: project, name: "@testscope/test") } context 'with a root namespace change' do + it 'allow the transfer' do + expect(transfer_service.execute(group)).to be true + expect(project.errors[:new_namespace]).to be_empty + end + end + + context 'with pending destruction package' do + before do + package.pending_destruction! + end + + it 'allow the transfer' do + expect(transfer_service.execute(group)).to be true + expect(project.errors[:new_namespace]).to be_empty + end + end + + context 'with namespaced packages present' do + let!(:package) { create(:npm_package, project: project, name: "@#{project.root_namespace.path}/test") } + it 'does not allow the transfer' do expect(transfer_service.execute(group)).to be false - expect(project.errors[:new_namespace]).to include("Root namespace can't be updated if project has NPM packages") + expect(project.errors[:new_namespace]).to include("Root namespace can't be updated if the project has NPM packages scoped to the current root level namespace.") end end @@ -39,7 +59,7 @@ RSpec.describe Projects::TransferService, feature_category: :projects do other_group.add_owner(user) end - it 'does allow the transfer' do + it 'allow the transfer' do expect(transfer_service.execute(other_group)).to be true expect(project.errors[:new_namespace]).to be_empty end diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb index de38d1ff9f8..af1843bae28 100644 --- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb @@ -138,6 +138,19 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do .not_to exceed_all_query_limit(control_count) end + context 'when user is not allowed to import projects' do + let(:user) { create(:user) } + let!(:group) { create(:group).tap { |group| group.add_developer(user) } } + + it 'returns 404' do + expect(stub_client(repos: [], orgs: [])).to receive(:repos) + + get :status, params: { namespace_id: group.id }, format: :html + + expect(response).to have_gitlab_http_status(:not_found) + end + end + context 'when filtering' do let(:repo_2) { repo_fake.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') } diff --git a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb index 44baadaaade..e94f063399d 100644 --- a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb +++ b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb @@ -19,4 +19,26 @@ RSpec.shared_examples 'import controller status' do expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id) end + + context 'when format is html' do + context 'when namespace_id is present' do + let!(:developer_group) { create(:group).tap { |g| g.add_developer(user) } } + + context 'when user cannot import projects' do + it 'returns 404' do + get :status, params: { namespace_id: developer_group.id }, format: :html + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when user can import projects' do + it 'returns 200' do + get :status, params: { namespace_id: group.id }, format: :html + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + end end |