diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-24 15:09:00 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-24 15:09:00 +0000 |
commit | c282dba898a4cb0645f88579339502a4e3778727 (patch) | |
tree | 94a6457ce4438e085c9ae43bc51a2b5a29787bf2 | |
parent | 2c2dd5e36c4ed5f09f488be288882d98f9124d12 (diff) | |
download | gitlab-ce-c282dba898a4cb0645f88579339502a4e3778727.tar.gz |
Add latest changes from gitlab-org/gitlab@master
60 files changed, 925 insertions, 127 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index b6a5c686f84..ae6f633fd60 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -348,10 +348,11 @@ RSpec/HaveGitlabHttpStatus: Include: - 'spec/support/shared_examples/**/*' - 'ee/spec/support/shared_examples/**/*' + - 'spec/features/**/*' + - 'ee/spec/features/**/*' Style/MultilineWhenThen: Enabled: false Style/FloatDivision: Enabled: false -
\ No newline at end of file diff --git a/app/assets/javascripts/graphql_shared/utils.js b/app/assets/javascripts/graphql_shared/utils.js new file mode 100644 index 00000000000..a262fbd9ac3 --- /dev/null +++ b/app/assets/javascripts/graphql_shared/utils.js @@ -0,0 +1,12 @@ +/** + * Ids generated by GraphQL endpoints are usually in the format + * gid://gitlab/Environments/123. This method extracts Id number + * from the Id path + * + * @param {String} gid GraphQL global ID + * @returns {Number} + */ +export const getIdFromGraphQLId = (gid = '') => + parseInt((gid || '').replace(/gid:\/\/gitlab\/.*\//g, ''), 10) || null; + +export default {}; diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index af9399a37bd..b192fb78631 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -1,5 +1,5 @@ <script> -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlBadge } from '@gitlab/ui'; import { visitUrl } from '../../lib/utils/url_utility'; import tooltip from '../../vue_shared/directives/tooltip'; import identicon from '../../vue_shared/components/identicon.vue'; @@ -17,6 +17,7 @@ export default { tooltip, }, components: { + GlBadge, GlLoadingIcon, identicon, itemCaret, @@ -62,6 +63,9 @@ export default { isGroup() { return this.group.type === 'group'; }, + isGroupPendingRemoval() { + return this.group.type === 'group' && this.group.pendingRemoval; + }, visibilityIcon() { return VISIBILITY_TYPE_ICON[this.group.visibility]; }, @@ -139,6 +143,9 @@ export default { <span v-html="group.description"> </span> </div> </div> + <div v-if="isGroupPendingRemoval"> + <gl-badge variant="warning">{{ __('pending removal') }}</gl-badge> + </div> <div class="metadata align-items-md-center d-flex flex-grow-1 flex-shrink-0 flex-wrap justify-content-md-between" > diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js index 214ac5e3db5..6a1197fa163 100644 --- a/app/assets/javascripts/groups/store/groups_store.js +++ b/app/assets/javascripts/groups/store/groups_store.js @@ -93,7 +93,7 @@ export default class GroupsStore { memberCount: rawGroupItem.number_users_with_delimiter, starCount: rawGroupItem.star_count, updatedAt: rawGroupItem.updated_at, - pendingRemoval: rawGroupItem.marked_for_deletion_at, + pendingRemoval: rawGroupItem.marked_for_deletion, }; } diff --git a/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql b/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql new file mode 100644 index 00000000000..fd3a4348509 --- /dev/null +++ b/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql @@ -0,0 +1,10 @@ +query getEnvironments($projectPath: ID!, $search: String) { + project(fullPath: $projectPath) { + data: environments(search: $search) { + environments: nodes { + name + id + } + } + } +} diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index 6303ed2b82f..e26e1457f55 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -1,7 +1,9 @@ import * as types from './mutation_types'; import axios from '~/lib/utils/axios_utils'; import createFlash from '~/flash'; +import { gqClient, parseEnvironmentsResponse, removeLeadingSlash } from './utils'; import trackDashboardLoad from '../monitoring_tracking_helper'; +import getEnvironments from '../queries/getEnvironments.query.graphql'; import statusCodes from '../../lib/utils/http_status'; import { backOff } from '../../lib/utils/common_utils'; import { s__, sprintf } from '../../locale'; @@ -187,26 +189,30 @@ export const fetchDeploymentsData = ({ state, dispatch }) => { }); }; -export const fetchEnvironmentsData = ({ state, dispatch }) => { - if (!state.environmentsEndpoint) { - return Promise.resolve([]); - } - return axios - .get(state.environmentsEndpoint) - .then(resp => resp.data) - .then(response => { - if (!response || !response.environments) { +export const fetchEnvironmentsData = ({ state, dispatch }) => + gqClient + .mutate({ + mutation: getEnvironments, + variables: { + projectPath: removeLeadingSlash(state.projectPath), + search: state.environmentsSearchTerm, + }, + }) + .then(resp => + parseEnvironmentsResponse(resp.data?.project?.data?.environments, state.projectPath), + ) + .then(environments => { + if (!environments) { createFlash( s__('Metrics|There was an error fetching the environments data, please try again'), ); } - dispatch('receiveEnvironmentsDataSuccess', response.environments); + dispatch('receiveEnvironmentsDataSuccess', environments); }) .catch(() => { dispatch('receiveEnvironmentsDataFailure'); createFlash(s__('Metrics|There was an error getting environments information.')); }); -}; /** * Set a new array of metrics to a panel group diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js index 616d2e9bfd8..56244a4ca66 100644 --- a/app/assets/javascripts/monitoring/stores/utils.js +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -1,8 +1,41 @@ import { omit } from 'lodash'; +import createGqClient from '~/lib/graphql'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; + +export const gqClient = createGqClient(); export const uniqMetricsId = metric => `${metric.metric_id}_${metric.id}`; /** + * Project path has a leading slash that doesn't work well + * with project full path resolver here + * https://gitlab.com/gitlab-org/gitlab/blob/5cad4bd721ab91305af4505b2abc92b36a56ad6b/app/graphql/resolvers/full_path_resolver.rb#L10 + * + * @param {String} str String with leading slash + * @returns {String} + */ +export const removeLeadingSlash = str => (str || '').replace(/^\/+/, ''); + +/** + * GraphQL environments API returns only id and name. + * For the environments dropdown we need metrics_path. + * This method parses the results and add neccessart attrs + * + * @param {Array} response Environments API result + * @param {String} projectPath Current project path + * @returns {Array} + */ +export const parseEnvironmentsResponse = (response = [], projectPath) => + (response || []).map(env => { + const id = getIdFromGraphQLId(env.id); + return { + ...env, + id, + metrics_path: `${projectPath}/environments/${id}/metrics`, + }; + }); + +/** * Metrics loaded from project-defined dashboards do not have a metric_id. * This method creates a unique ID combining metric_id and id, if either is present. * This is hopefully a temporary solution until BE processes metrics before passing to fE diff --git a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue index 7530c1dfcaf..28f4ef62242 100644 --- a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue +++ b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue @@ -1,5 +1,8 @@ <script> -import { mapActions } from 'vuex'; +import { mapActions, mapState } from 'vuex'; +import { GlAlert } from '@gitlab/ui'; +import { sprintf, s__ } from '~/locale'; + import { FETCH_SETTINGS_ERROR_MESSAGE } from '../constants'; import SettingsForm from './settings_form.vue'; @@ -7,6 +10,23 @@ import SettingsForm from './settings_form.vue'; export default { components: { SettingsForm, + GlAlert, + }, + computed: { + ...mapState(['isDisabled']), + notAvailableMessage() { + return sprintf( + s__( + 'ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}', + ), + { + linkStart: + '<a href="https://gitlab.com/gitlab-org/gitlab/issues/196124" target="_blank" rel="noopener noreferrer">', + linkEnd: '</a>', + }, + false, + ); + }, }, mounted() { this.fetchSettings().catch(() => @@ -34,6 +54,9 @@ export default { }} </li> </ul> - <settings-form ref="settings-form" /> + <settings-form v-if="!isDisabled" /> + <gl-alert v-else :dismissible="false"> + <p v-html="notAvailableMessage"></p> + </gl-alert> </div> </template> diff --git a/app/assets/javascripts/registry/settings/store/actions.js b/app/assets/javascripts/registry/settings/store/actions.js index 21a2008fef6..d0379d05164 100644 --- a/app/assets/javascripts/registry/settings/store/actions.js +++ b/app/assets/javascripts/registry/settings/store/actions.js @@ -4,7 +4,13 @@ import * as types from './mutation_types'; export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data); export const updateSettings = ({ commit }, data) => commit(types.UPDATE_SETTINGS, data); export const toggleLoading = ({ commit }) => commit(types.TOGGLE_LOADING); -export const receiveSettingsSuccess = ({ commit }, data = {}) => commit(types.SET_SETTINGS, data); +export const receiveSettingsSuccess = ({ commit }, data) => { + if (data) { + commit(types.SET_SETTINGS, data); + } else { + commit(types.SET_IS_DISABLED, true); + } +}; export const resetSettings = ({ commit }) => commit(types.RESET_SETTINGS); export const fetchSettings = ({ dispatch, state }) => { diff --git a/app/assets/javascripts/registry/settings/store/mutation_types.js b/app/assets/javascripts/registry/settings/store/mutation_types.js index db499ffa761..2d071567c1f 100644 --- a/app/assets/javascripts/registry/settings/store/mutation_types.js +++ b/app/assets/javascripts/registry/settings/store/mutation_types.js @@ -3,3 +3,4 @@ export const UPDATE_SETTINGS = 'UPDATE_SETTINGS'; export const TOGGLE_LOADING = 'TOGGLE_LOADING'; export const SET_SETTINGS = 'SET_SETTINGS'; export const RESET_SETTINGS = 'RESET_SETTINGS'; +export const SET_IS_DISABLED = 'SET_IS_DISABLED'; diff --git a/app/assets/javascripts/registry/settings/store/mutations.js b/app/assets/javascripts/registry/settings/store/mutations.js index 25a67cc6973..b773f2dd44c 100644 --- a/app/assets/javascripts/registry/settings/store/mutations.js +++ b/app/assets/javascripts/registry/settings/store/mutations.js @@ -16,6 +16,9 @@ export default { state.settings = settings; state.original = Object.freeze(settings); }, + [types.SET_IS_DISABLED](state, isDisabled) { + state.isDisabled = isDisabled; + }, [types.RESET_SETTINGS](state) { state.settings = { ...state.original }; }, diff --git a/app/assets/javascripts/registry/settings/store/state.js b/app/assets/javascripts/registry/settings/store/state.js index 50c882e1839..582e18e5465 100644 --- a/app/assets/javascripts/registry/settings/store/state.js +++ b/app/assets/javascripts/registry/settings/store/state.js @@ -8,6 +8,10 @@ export default () => ({ */ isLoading: false, /* + * Boolean to determine if the user is allowed to interact with the form + */ + isDisabled: false, + /* * This contains the data shown and manipulated in the UI * Has the following structure: * { diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index 1358b3aa6dd..9e99c92d3f2 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -56,6 +56,7 @@ .gl-bg-green-100 { @include gl-bg-green-100;} .gl-text-blue-500 { @include gl-text-blue-500; } +.gl-text-gray-700 { @include gl-text-gray-700; } .gl-text-gray-900 { @include gl-text-gray-900; } .gl-text-red-700 { @include gl-text-red-700; } .gl-text-orange-700 { @include gl-text-orange-700; } diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 5455cefdc8e..0245c00aacb 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -6,8 +6,7 @@ class Admin::GroupsController < Admin::ApplicationController before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update] def index - @groups = Group.with_statistics.with_route - @groups = @groups.sort_by_attribute(@sort = params[:sort]) + @groups = groups.sort_by_attribute(@sort = params[:sort]) @groups = @groups.search(params[:name]) if params[:name].present? @groups = @groups.page(params[:page]) end @@ -75,6 +74,10 @@ class Admin::GroupsController < Admin::ApplicationController private + def groups + Group.with_statistics.with_route + end + def group @group ||= Group.find_by_full_path(params[:id]) end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 3000cfb678e..b8d7685c2cf 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -709,4 +709,10 @@ module ProjectsHelper def show_visibility_confirm_modal?(project) project.unlink_forks_upon_visibility_decrease_enabled? && project.visibility_level > Gitlab::VisibilityLevel::PRIVATE && project.forks_count > 0 end + + def settings_container_registry_expiration_policy_available?(project) + Feature.enabled?(:registry_retention_policies_settings, project) && + Gitlab.config.registry.enabled && + can?(current_user, :read_container_image, project) + end end diff --git a/app/models/concerns/loaded_in_group_list.rb b/app/models/concerns/loaded_in_group_list.rb index fc15c6d55ed..79ff82d9f99 100644 --- a/app/models/concerns/loaded_in_group_list.rb +++ b/app/models/concerns/loaded_in_group_list.rb @@ -73,3 +73,5 @@ module LoadedInGroupList @member_count ||= try(:preloaded_member_count) || users.count end end + +LoadedInGroupList::ClassMethods.prepend_if_ee('EE::LoadedInGroupList::ClassMethods') diff --git a/app/models/group.rb b/app/models/group.rb index b642b177df1..ea5d46e23f4 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -467,6 +467,10 @@ class Group < Namespace import_export_upload&.export_file end + def adjourned_deletion? + false + end + private def update_two_factor_requirement diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index 3444e423235..855858ff929 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -10,6 +10,7 @@ = storage_counter(group.storage_size) = render_if_exists 'admin/namespace_plan_badge', namespace: group + = render_if_exists 'admin/groups/marked_for_deletion_badge', group: group %span = icon('bookmark') diff --git a/app/views/groups/settings/_advanced.html.haml b/app/views/groups/settings/_advanced.html.haml index 307309c6ca3..2734ab538a0 100644 --- a/app/views/groups/settings/_advanced.html.haml +++ b/app/views/groups/settings/_advanced.html.haml @@ -39,12 +39,5 @@ %li= s_("GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.") = f.submit s_('GroupSettings|Transfer group'), class: 'btn btn-warning' -.sub-section - %h4.danger-title= _('Remove group') - = form_tag(@group, method: :delete) do - %p - = _('Removing group will cause all child projects and resources to be removed.') - %br - %strong= _('Removed group can not be restored!') - - = button_to _('Remove group'), '#', class: 'btn btn-remove js-confirm-danger', data: { 'confirm-danger-message' => remove_group_message(@group) } += render 'groups/settings/remove', group: @group += render_if_exists 'groups/settings/restore', group: @group diff --git a/app/views/groups/settings/_permanent_deletion.html.haml b/app/views/groups/settings/_permanent_deletion.html.haml new file mode 100644 index 00000000000..31e2bac70be --- /dev/null +++ b/app/views/groups/settings/_permanent_deletion.html.haml @@ -0,0 +1,9 @@ +.sub-section + %h4.danger-title= _('Remove group') + = form_tag(group, method: :delete) do + %p + = _('Removing group will cause all child projects and resources to be removed.') + %br + %strong= _('Removed group can not be restored!') + + = button_to _('Remove group'), '#', class: 'btn btn-remove js-confirm-danger', data: { 'confirm-danger-message' => remove_group_message(group) } diff --git a/app/views/groups/settings/_remove.html.haml b/app/views/groups/settings/_remove.html.haml new file mode 100644 index 00000000000..a617467019a --- /dev/null +++ b/app/views/groups/settings/_remove.html.haml @@ -0,0 +1,5 @@ +- if group.adjourned_deletion? + = render_if_exists 'groups/settings/adjourned_deletion', group: group +- else + = render 'groups/settings/permanent_deletion', group: group + diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 457d05b4a97..4916c4651dd 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -9,6 +9,8 @@ = render 'groups/home_panel' + = render_if_exists 'groups/self_or_ancestor_marked_for_deletion_notice', group: @group + .groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } } .top-area.group-nav-container.justify-content-between .scrolling-tabs-container.inner-page-scroll-tabs diff --git a/app/views/kaminari/gitlab/_first_page.html.haml b/app/views/kaminari/gitlab/_first_page.html.haml deleted file mode 100644 index 3b7d4a1c578..00000000000 --- a/app/views/kaminari/gitlab/_first_page.html.haml +++ /dev/null @@ -1,9 +0,0 @@ --# Link to the "First" page --# available local variables --# url: url to the first page --# current_page: a page object for the currently displayed page --# total_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%li.page-item.js-first-button - = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, remote: remote, class: 'page-link' diff --git a/app/views/kaminari/gitlab/_last_page.html.haml b/app/views/kaminari/gitlab/_last_page.html.haml deleted file mode 100644 index 7836e17f877..00000000000 --- a/app/views/kaminari/gitlab/_last_page.html.haml +++ /dev/null @@ -1,9 +0,0 @@ --# Link to the "Last" page --# available local variables --# url: url to the last page --# current_page: a page object for the currently displayed page --# total_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%li.page-item.js-last-button - = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {remote: remote, class: 'page-link'} diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml index a7fa1a21a6c..9572dd91330 100644 --- a/app/views/kaminari/gitlab/_next_page.html.haml +++ b/app/views/kaminari/gitlab/_next_page.html.haml @@ -9,4 +9,6 @@ - page_url = current_page.last? ? '#' : url %li.page-item.js-next-button{ class: ('disabled' if current_page.last?) } - = link_to raw(t 'views.pagination.next'), page_url, rel: 'next', remote: remote, class: 'page-link' + = link_to page_url, rel: 'next', remote: remote, class: 'page-link' do + = s_('Pagination|Next') + = sprite_icon('angle-right', size: 8) diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml index d0dc1784540..33e00256100 100644 --- a/app/views/kaminari/gitlab/_page.html.haml +++ b/app/views/kaminari/gitlab/_page.html.haml @@ -6,5 +6,9 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li.page-item.js-pagination-page{ class: [active_when(page.current?), ('sibling' if page.next? || page.prev?), ('d-none d-md-block' if !page.current?) ] } +%li.page-item.js-pagination-page{ class: [active_when(page.current?), + ('sibling' if page.next? || page.prev?), + ('js-first-button' if page.first?), + ('js-last-button' if page.last?), + ('d-none d-md-block' if !page.current?) ] } = link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil, class: 'page-link' } diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml index ac9e274dbc7..1b2edc0ad22 100644 --- a/app/views/kaminari/gitlab/_paginator.html.haml +++ b/app/views/kaminari/gitlab/_paginator.html.haml @@ -8,14 +8,10 @@ = paginator.render do .gl-pagination.prepend-top-default %ul.pagination.justify-content-center - - unless current_page.first? - = first_page_tag unless total_pages < 5 # As kaminari will always show the first 5 pages = prev_page_tag - each_page do |page| - - if page.left_outer? || page.right_outer? || page.inside_window? + - if page.left_outer? || page.right_outer? || page.inside_window? || page.first? || page.last? = page_tag page - elsif !page.was_truncated? = gap_tag = next_page_tag - - unless current_page.last? - = last_page_tag unless total_pages < 5 diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml index 12b0e106a62..4ba7ab6488a 100644 --- a/app/views/kaminari/gitlab/_prev_page.html.haml +++ b/app/views/kaminari/gitlab/_prev_page.html.haml @@ -9,4 +9,6 @@ - page_url = current_page.first? ? '#' : url %li.page-item.js-previous-button{ class: ('disabled' if current_page.first?) } - = link_to raw(t 'views.pagination.previous'), page_url, rel: 'prev', remote: remote, class: 'page-link' + = link_to page_url, rel: 'prev', remote: remote, class: 'page-link' do + = sprite_icon('angle-left', size: 8) + = s_('Pagination|Prev') diff --git a/app/views/kaminari/gitlab/_without_count.html.haml b/app/views/kaminari/gitlab/_without_count.html.haml index f780400ebcb..d13f6ca5fa8 100644 --- a/app/views/kaminari/gitlab/_without_count.html.haml +++ b/app/views/kaminari/gitlab/_without_count.html.haml @@ -2,7 +2,11 @@ %ul.pagination.justify-content-center - if previous_path %li.page-item.prev - = link_to(t('views.pagination.previous'), previous_path, rel: 'prev', class: 'page-link') + = link_to previous_path, rel: 'prev', class: 'page-link' do + = sprite_icon('angle-left', size: 8) + = s_('Pagination|Prev') - if next_path %li.page-item.next - = link_to(t('views.pagination.next'), next_path, rel: 'next', class: 'page-link') + = link_to next_path, rel: 'next', class: 'page-link' do + = s_('Pagination|Next') + = sprite_icon('angle-right', size: 8) diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index a65afeecc17..1358077f2b2 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -62,12 +62,12 @@ .settings-content = render 'projects/triggers/index' -- if Feature.enabled?(:registry_retention_policies_settings, @project) +- if settings_container_registry_expiration_policy_available?(@project) %section.settings.no-animate#js-registry-policies{ class: ('expanded' if expanded) } .settings-header %h4 = _("Container Registry tag expiration policy") - = link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'retention-and-expiration-policy'), target: '_blank', rel: 'noopener noreferrer' + = link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy'), target: '_blank', rel: 'noopener noreferrer' %button.btn.js-settings-toggle{ type: 'button' } = expanded ? _('Collapse') : _('Expand') %p diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 8f13806e8cd..17bc10af58a 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -20,6 +20,7 @@ = render "archived_notice", project: @project = render_if_exists "projects/marked_for_deletion_notice", project: @project + = render_if_exists "projects/ancestor_group_marked_for_deletion_notice", project: @project - view_path = @project.default_view diff --git a/changelogs/unreleased/33071-update-kaminari-templates.yml b/changelogs/unreleased/33071-update-kaminari-templates.yml new file mode 100644 index 00000000000..7e43cce729d --- /dev/null +++ b/changelogs/unreleased/33071-update-kaminari-templates.yml @@ -0,0 +1,5 @@ +--- +title: Update Kaminari templates to match gl-pagination's markup +merge_request: 23582 +author: +type: other diff --git a/changelogs/unreleased/33257-prevent-accidental-deletions-via-soft-delete-for-groups-ui-changes.yml b/changelogs/unreleased/33257-prevent-accidental-deletions-via-soft-delete-for-groups-ui-changes.yml new file mode 100644 index 00000000000..c3c722339cd --- /dev/null +++ b/changelogs/unreleased/33257-prevent-accidental-deletions-via-soft-delete-for-groups-ui-changes.yml @@ -0,0 +1,5 @@ +--- +title: Add UI for 'soft-delete for groups' feature +merge_request: 19483 +author: +type: added diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index 4dd37d24855..f1c9604b77f 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -885,6 +885,12 @@ you are seeing Gitaly errors. You can control the log level of the gRPC client with the `GRPC_LOG_LEVEL` environment variable. The default level is `WARN`. +You can run a GRPC trace with: + +```sh +GRPC_TRACE=all GRPC_VERBOSITY=DEBUG sudo gitlab-rake gitlab:gitaly:check +``` + ### Observing `gitaly-ruby` traffic [`gitaly-ruby`](#gitaly-ruby) is an internal implementation detail of Gitaly, diff --git a/doc/api/groups.md b/doc/api/groups.md index 156dda52ab0..411e5e5f8ca 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -633,7 +633,7 @@ Only available to group owners and administrators. This endpoint either: - Removes group, and queues a background job to delete all projects in the group as well. -- Since GitLab 12.8, on [Premium](https://about.gitlab.com/pricing/premium/) or higher tiers, marks a group for deletion. The deletion will happen 7 days later by default, but this can be changed in the [instance settings](../user/admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only). +- Since [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/issues/33257), on [Premium or Silver](https://about.gitlab.com/pricing/) or higher tiers, marks a group for deletion. The deletion will happen 7 days later by default, but this can be changed in the [instance settings](../user/admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only). ``` DELETE /groups/:id diff --git a/doc/api/projects.md b/doc/api/projects.md index 6467f2626de..d67d8949571 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1767,7 +1767,7 @@ Example response: This endpoint either: - Removes a project including all associated resources (issues, merge requests etc). -- From GitLab 12.6 on Premium or higher tiers, marks a project for deletion. Actual +- From [GitLab 12.6](https://gitlab.com/gitlab-org/gitlab/issues/32935) on [Premium or Silver](https://about.gitlab.com/pricing/) or higher tiers, marks a project for deletion. Actual deletion happens after number of days specified in [instance settings](../user/admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only). diff --git a/doc/development/documentation/site_architecture/release_process.md b/doc/development/documentation/site_architecture/release_process.md index 76bd74b0bdc..51a02528758 100644 --- a/doc/development/documentation/site_architecture/release_process.md +++ b/doc/development/documentation/site_architecture/release_process.md @@ -62,6 +62,12 @@ The single docs version must be created before the release merge request, but this needs to happen when the stable branches for all products have been created. 1. Make sure you're on the root path of the `gitlab-docs` repo. +1. Make sure your `master` is updated: + + ```sh + git pull origin master + ``` + 1. Run the raketask to create the single version: ```sh @@ -70,6 +76,17 @@ this needs to happen when the stable branches for all products have been created A new `Dockerfile.12.0` should have been created and committed to a new branch. +1. Edit `.gitlab-ci.yml` and replace the `BRANCH_` variables with their respective + upstream stable branch. For example, 12.6 would look like: + + ```yaml + variables: + BRANCH_EE: '12-6-stable-ee' + BRANCH_OMNIBUS: '12-6-stable' + BRANCH_RUNNER: '12-6-stable' + BRANCH_CHARTS: '2-6-stable' + ``` + 1. Push the newly created branch, but **don't create a merge request**. Once you push, the `image:docker-singe` job will create a new Docker image tagged with the branch name you created in the first step. In the end, the @@ -95,6 +112,7 @@ version and rotates the old one: 1. Create a branch `release-X-Y`: ```sh + git checkout master git checkout -b release-12-0 ``` @@ -143,16 +161,12 @@ version and rotates the old one: The versions dropdown is in a way "hardcoded". When the site is built, it looks at the contents of `content/_data/versions.yaml` and based on that, the dropdown is populated. So, older branches will have different content, which means the -dropdown will be one or more releases behind. Remember that the new changes of +dropdown will list one or more releases behind. Remember that the new changes of the dropdown are included in the unmerged `release-X-Y` branch. The content of `content/_data/versions.yaml` needs to change for all online versions: -1. Before creating the merge request, [disable the scheduled pipeline](https://gitlab.com/gitlab-org/gitlab-docs/pipeline_schedules/228/edit) - by unchecking the "Active" option. Since all steps must run in sequence, we need - to do this to avoid race conditions in the event some previous versions are - updated before the release merge request is merged. 1. Run the raketask that will create all the respective merge requests needed to update the dropdowns and will be set to automatically be merged when their pipelines succeed. The `release-X-Y` branch needs to be present locally, @@ -162,7 +176,9 @@ versions: ./bin/rake release:dropdowns ``` -Once all are merged, proceed to the following and final step. +1. [Visit the merge requests page](https://gitlab.com/gitlab-org/gitlab-docs/merge_requests?label_name%5B%5D=release>) + to check that their pipelines pass, and once all are merged, proceed to the + following and final step. TIP: **Tip:** In case a pipeline fails, see [troubleshooting](#troubleshooting). @@ -176,8 +192,8 @@ you need to only babysit the pipelines and make sure they don't fail: 1. Check the [pipelines page](https://gitlab.com/gitlab-org/gitlab-docs/pipelines) and make sure all stable branches have green pipelines. 1. After all the pipelines of the online versions succeed, merge the release merge request. -1. Finally, re-activate the [scheduled pipeline](https://gitlab.com/gitlab-org/gitlab-docs/pipeline_schedules/228/edit), - save it, and hit the play button to get it started. +1. Finally, run the [Build docker images weekly](https://gitlab.com/gitlab-org/gitlab-docs/pipeline_schedules) + pipeline that will build the `:latest` and `:archives` Docker images. Once the scheduled pipeline succeeds, the docs site will be deployed with all new versions online. @@ -214,6 +230,10 @@ Releasing a new version is a long process that involves many moving parts. ### `test_internal_links_and_anchors` failing on dropdown merge requests +NOTE: **Note:** +We now pin versions in the `.gitlab-ci.yml` of the respective branch, +so the steps below are deprecated. + When [updating the dropdown for the stable versions](#4-update-the-dropdown-for-all-online-versions), there may be cases where some links might fail. The process of how the dropdown MRs are created have a caveat, and that is that the tests run by @@ -229,7 +249,7 @@ branches for 12.2 were used, this wouldn't have failed, but as we can see from the [`compile_dev` job](https://gitlab.com/gitlab-org/gitlab-docs/-/jobs/328042427), the `master` branches were pulled. -To fix this, you need to [re-run the pipeline](https://gitlab.com/gitlab-org/gitlab-docs/pipelines/new) +To fix this, [re-run the pipeline](https://gitlab.com/gitlab-org/gitlab-docs/pipelines/new) for the `update-12-2-for-release-12-4` branch, by including the following environment variables: - `BRANCH_CE` set to `12-2-stable` diff --git a/doc/development/permissions.md b/doc/development/permissions.md index 5d409c9461e..9e67079d1bc 100644 --- a/doc/development/permissions.md +++ b/doc/development/permissions.md @@ -61,3 +61,42 @@ is stored in the `project_authorizations` table. Confidential issues can be accessed only by project members who are at least reporters (they can't be accessed by guests). Additionally they can be accessed by their authors and assignees. + +### Licensed features + +Some features can be accessed only if the user has the correct license plan. + +## Permission dependencies + +Feature policies can be quite complex and consist of multiple rules. +Quite often, one permission can be based on another. + +Designing good permissions means reusing existing permissions as much as possible +and making access to features granular. + +In the case of a complex resource, it should be broken into smaller pieces of information +and each piece should be granted a different permission. + +A good example in this case is the _Merge Request widget_ and the _Security reports_. +Depending on the visibility level of the _Pipelines_, the _Security reports_ will be either visible +in the widget or not. So, the _Merge Request widget_, the _Pipelines_, and the _Security reports_, +have separate permissions. Moreover, the permissions for the _Merge Request widget_ +and the _Pipelines_ are dependencies of the _Security reports_. + +### Permission dependencies of Secure features + +Secure features have complex permissions since these features are integrated +into different features like Merge Requests and CI flow. + + Here is a list of some permission dependencies. + +| Activity level | Resource | Locations |Permission dependency| +|----------------|----------|-----------|-----| +| View | License information | Dependency list, License Compliance | Can view repo | +| View | Dependency information | Dependency list, License Compliance | Can view repo | +| View | Vulnerabilities information | Dependency list | Can view security findings | +| View | Black/Whitelisted licenses for the project | License Compliance, Merge request | Can view repo | +| View | Security findings | Merge Request, CI job page, Pipeline security tab | Can read the project and CI jobs | +| View | Vulnerability feedback | Merge Request | Can read security findings | +| View | Dependency List page | Project | Can access Dependency information | +| View | License Compliance page | Project | Can access License information| diff --git a/doc/user/group/index.md b/doc/user/group/index.md index bf0a8c6bfbd..d43b3160391 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -314,6 +314,30 @@ If you want to retain ownership over the original namespace and protect the URL redirects, then instead of changing a group's path or renaming a username, you can create a new group and transfer projects to it. +### Remove a group + +To remove a group and its contents: + +1. Navigate to your group's **{settings}** **Settings > General** page. +1. Expand the **Path, transfer, remove** section. +1. In the Remove group section, click the **Remove group** button. +1. Confirm the action when asked to. + +This action either: + +- Removes the group, and also queues a background job to delete all projects in that group. +- Since [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/issues/33257), on [Premium or Silver](https://about.gitlab.com/pricing/premium/) or higher tiers, marks a group for deletion. The deletion will happen 7 days later by default, but this can be changed in the [instance settings](../admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only). + +### Restore a group **(PREMIUM)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/33257) in GitLab 12.8. + +To restore a group that is marked for deletion: + +1. Navigate to your group's **{settings}** **Settings > General** page. +1. Expand the **Path, transfer, remove** section. +1. In the Restore group section, click the **Restore group** button. + #### Enforce 2FA to group members Add a security layer to your group by diff --git a/doc/user/packages/container_registry/img/expiration-policy-app.png b/doc/user/packages/container_registry/img/expiration-policy-app.png Binary files differnew file mode 100644 index 00000000000..38de4d85ae5 --- /dev/null +++ b/doc/user/packages/container_registry/img/expiration-policy-app.png diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md index 877379705de..97831d65759 100644 --- a/doc/user/packages/container_registry/index.md +++ b/doc/user/packages/container_registry/index.md @@ -138,6 +138,47 @@ Example of using a token: docker login registry.example.com -u <username> -p <token> ``` +## Expiration policy + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/15398) in GitLab 12.8. + +It is possible to create a per-project expiration policy, so that you can make sure that +older tags and images are regularly removed from the Container Registry. + +### Managing project expiration policy through the API + +You can set, update, and disable the expiration policies using the GitLab API. + +Examples: + +- Select all tags, keep at least 1 tag per image, expire any tag older than 14 days, run once a month, and the policy is enabled: + + ```bash + curl --request PUT --header 'Content-Type: application/json;charset=UTF-8' --header "PRIVATE-TOKEN: <your_access_token>" --data-binary '{"container_expiration_policy_attributes":{"cadence":"1month","enabled":true,"keep_n":1,"older_than":"14d","name_regex":".*"}' 'https://gitlab.example.com/api/v4/projects/2' + ``` + +- Select only tags with a name that contains `stable`, keep at least 50 tag per image, expire any tag older than 7 days, run every day, and the policy is enabled: + + ```bash + curl --request PUT --header 'Content-Type: application/json;charset=UTF-8' --header "PRIVATE-TOKEN: <your_access_token>" --data-binary '{"container_expiration_policy_attributes":{"cadence":"1day","enabled":true,"keep_n":50"older_than":"7d","name_regex":"*stable"}' 'https://gitlab.example.com/api/v4/projects/2' + ``` + +See the API documentation for further details: [Edit project](../../../api/projects.md#edit-project). + +### Managing project expiration policy through the UI + +To manage project expiration policy, navigate to **Settings > CI/CD > Container Registry tag expiration policy**. + +![Expiration Policy App](img/expiration-policy-app.png) + +The UI allows you to configure the following: + +- **Expiration policy:** enable or disable the expiration policy. +- **Expiration interval:** how long tags are exempt from being deleted. +- **Expiration schedule:** how often the cron job checking the tags should run. +- **Expiration latest:** how many tags to _always_ keep for each image. +- **Expire Docker tags with regex matching:** the regex used to determine what tags should be expired. To qualify all tags for expiration, use the default value of `.*`. + ## Troubleshooting the GitLab Container Registry ### Docker connection error diff --git a/doc/user/packages/index.md b/doc/user/packages/index.md index c58effac59d..80a3b1e1392 100644 --- a/doc/user/packages/index.md +++ b/doc/user/packages/index.md @@ -27,4 +27,5 @@ NOTE: **Note** We are especially interested in adding support for [PyPi](https:/ Learning how to use the GitLab Package Registry will help you build your own custom package workflow. -[Use a project as a package registry](./workflows/project_registry.md) to publish all of your packages to one project. +- [Use a project as a package registry](./workflows/project_registry.md) to publish all of your packages to one project. +- [Working with a monorepo](./workflows/monorepo.md): Learn how to publish multiple different packages from one monorepo project. diff --git a/doc/user/packages/maven_repository/index.md b/doc/user/packages/maven_repository/index.md index da5139fcaf9..ea052703416 100644 --- a/doc/user/packages/maven_repository/index.md +++ b/doc/user/packages/maven_repository/index.md @@ -338,3 +338,53 @@ The next time the `deploy` job runs, it will copy `ci_settings.xml` to the user's home location (in this case the user is `root` since it runs in a Docker container), and Maven will utilize the configured CI [environment variables](../../../ci/variables/README.md#predefined-environment-variables). + +## Troubleshooting + +### Useful Maven command line options + +There's some [maven command line options](https://maven.apache.org/ref/current/maven-embedder/cli.html) +which maybe useful when doing tasks with GitLab CI. + +- File transfer progress can make the CI logs hard to read. + Option `-ntp,--no-transfer-progress` was added in + [3.6.1](https://maven.apache.org/docs/3.6.1/release-notes.html#User_visible_Changes). + Alternatively, look at `-B,--batch-mode` + [or lower level logging changes.](https://stackoverflow.com/questions/21638697/disable-maven-download-progress-indication) + +- Specify where to find the POM file (`-f,--file`): + + ```yaml + package: + script: + - 'mvn --no-transfer-progress -f helloworld/pom.xml package' + ``` + +- Specify where to find the user settings (`-s,--settings`) instead of + [the default location](https://maven.apache.org/settings.html). There's also a `-gs,--global-settings` option: + + ```yaml + package: + script: + - 'mvn -s settings/ci.xml package' + ``` + +### Verifying your Maven settings + +If you encounter issues within CI that relate to the `settings.xml` file, it might be useful +to add an additional script task or job to +[verify the effective settings](https://maven.apache.org/plugins/maven-help-plugin/effective-settings-mojo.html). + +The help plugin can also provide +[system properties](https://maven.apache.org/plugins/maven-help-plugin/system-mojo.html), including environment variables: + +```yaml +mvn-settings: + script: + - 'mvn help:effective-settings' + +package: + script: + - 'mvn help:system' + - 'mvn package' +``` diff --git a/doc/user/packages/workflows/monorepo.md b/doc/user/packages/workflows/monorepo.md new file mode 100644 index 00000000000..9f281d2b31c --- /dev/null +++ b/doc/user/packages/workflows/monorepo.md @@ -0,0 +1,119 @@ +# Monorepo package management workflows + +Oftentimes, one project or Git repository may contain multiple different +subprojects or submodules that all get packaged and published individually. + +## Publishing different packages to the parent project + +The number and name of packages you can publish to one project is not limited. +You can accomplish this by setting up different configuration files for each +package. See the documentation for the package manager of your choice since +each will have its own specific files and instructions to follow to publish +a given package. + +Here, we will walk through how to do this with [NPM](../npm_registry/index.md). + +Let us say we have a project structure like so: + +```plaintext +MyProject/ + |- src/ + | |- components/ + | |- Foo/ + |- package.json +``` + +`MyProject` is the parent project, which contains a sub-project `Foo` in the +`components` directory. We would like to publish packages for both `MyProject` +as well as `Foo`. + +Following the instructions in the +[GitLab NPM registry documentation](../npm_registry/index.md), +publishing `MyProject` consists of modifying the `package.json` file with a +`publishConfig` section, as well as either modifying your local NPM config with +CLI commands like `npm config set`, or saving a `.npmrc` file in the root of the +project specifying these config settings. + +If you follow the instructions you can publish `MyProject` by running +`npm publish` from the root directory. + +Publishing `Foo` is almost exactly the same, you simply have to follow the steps +while in the `Foo` directory. `Foo` will need it's own `package.json` file, +which can be added manually or using `npm init`. And it will need it's own +configuration settings. Since you are publishing to the same place, if you +used `npm config set` to set the registry for the parent project, then no +additional setup is necessary. If you used a `.npmrc` file, you will need an +additional `.npmrc` file in the `Foo` directory (be sure to add `.npmrc` files +to the `.gitignore` file or use environment variables in place of your access +tokens to preven them from being exposed). It can be identical to the +one you used in `MyProject`. You can now run `npm publish` from the `Foo` +directory and you will be able to publish `Foo` separately from `MyProject` + +A similar process could be followed for Conan packages, instead of dealing with +`.npmrc` and `package.json`, you will just be dealing with `conanfile.py` in +multiple locations within the project. + +## Publishing to other projects + +A package is associated with a project on GitLab, but the package does not +need to be associated with the code in that project. Notice when configuring +NPM or Maven, you only use the `Project ID` to set the registry URL that the +package will be uploaded to. If you set this to any project that you have +access to and update any other config similarly depending on the package type, +your packages will be published to that project. This means you can publish +multiple packages to one project, even if their code does not exist in the same +place. See the [project registry workflow documentation](./project_registry.md) +for more details. + +## CI workflows for automating packaging + +CI pipelines open an entire world of possibilities for dealing with the patterns +described in the previous sections. A common desire would be to publish +specific packages only if changes were made to those directories. + +Using the example project above, this `gitlab-ci.yml` file will publish +`Foo` anytime changes are made to the `Foo` directory on the `master` branch, +and publish `MyPackage` anytime changes are made to anywhere _except_ the `Foo` +directory on the `master` branch. + +```sh +stages: + - build + +.default-rule: &default-rule + if: '$CI_MERGE_REQUEST_IID || $CI_COMMIT_REF_SLUG == "master"' + +.foo-package: + variables: + PACKAGE: "Foo" + before_script: + - cd src/components/Foo + only: + changes: + - "src/components/Foo/**/*" + +.parent-package: + variables: + PACKAGE: "MyPackage" + except: + changes: + - "src/components/Foo/**/*" + +.build-package: + stage: build + script: + - echo "Building $PACKAGE" + - npm publish + rules: + - <<: *default-rule + +build-foo-package: + extends: + - .build-package + - .foo-package + +build-my-project-package: + extends: + - .build-package + - .parent-package +``` diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index bc1053f5bf2..b4d4ec7a922 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -177,6 +177,31 @@ namespace if needed. [permissions]: ../../permissions.md#project-members-permissions +#### Remove a project + +NOTE: **Note:** +Only project owners and admins have [permissions]((../../permissions.md#project-members-permissions) to remove a project. + +To remove a project: + +1. Navigate to your project, and select **{settings}** **Settings > General > Advanced**. +1. In the Remove project section, click the **Remove project** button. +1. Confirm the action when asked to. + +This action either: + +- Removes a project including all associated resources (issues, merge requests etc). +- Since [GitLab 12.6](https://gitlab.com/gitlab-org/gitlab/issues/32935), on [Premium or Silver](https://about.gitlab.com/pricing/) or higher tiers, marks a project for deletion. The deletion will happen 7 days later by default, but this can be changed in the [instance settings](../../admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only). + +### Restore a project **(PREMIUM)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/32935) in GitLab 12.6. + +To restore a project that is marked for deletion: + +1. Navigate to your project, and select **{settings}** **Settings > General > Advanced**. +1. In the Restore project section, click the **Restore project** button. + ## Operations settings ### Error Tracking diff --git a/lib/gitlab/quick_actions/extractor.rb b/lib/gitlab/quick_actions/extractor.rb index e04d6f250b1..6f87968e286 100644 --- a/lib/gitlab/quick_actions/extractor.rb +++ b/lib/gitlab/quick_actions/extractor.rb @@ -34,26 +34,55 @@ module Gitlab def extract_commands(content, only: nil) return [content, []] unless content - content = content.dup + content, commands = perform_regex(content, only: only) - commands = [] + perform_substitutions(content, commands) + end + # Encloses quick action commands into code span markdown + # avoiding them being executed, for example, when sent via email + # to GitLab service desk. + # Example: /label ~label1 becomes `/label ~label1` + def redact_commands(content) + return "" unless content + + content, _ = perform_regex(content, redact: true) + + content + end + + private + + def perform_regex(content, only: nil, redact: false) + commands = [] + content = content.dup content.delete!("\r") + content.gsub!(commands_regex(only: only)) do - if $~[:cmd] - commands << [$~[:cmd].downcase, $~[:arg]].reject(&:blank?) - '' - else - $~[0] - end + command, output = process_commands($~, redact) + commands << command + output end - content, commands = perform_substitutions(content, commands) - - [content.rstrip, commands] + [content.rstrip, commands.reject(&:empty?)] end - private + def process_commands(matched_text, redact) + output = matched_text[0] + command = [] + + if matched_text[:cmd] + command = [matched_text[:cmd].downcase, matched_text[:arg]].reject(&:blank?) + output = '' + + if redact + output = "`/#{matched_text[:cmd]}#{" " + matched_text[:arg] if matched_text[:arg]}`" + output += "\n" if matched_text[0].include?("\n") + end + end + + [command, output] + end # Builds a regular expression to match known commands. # First match group captures the command name and diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c51b29d24dc..025d21e1c25 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -528,6 +528,9 @@ msgstr "" msgid "+ %{numberOfHiddenAssignees} more" msgstr "" +msgid "+%{approvers} more approvers" +msgstr "" + msgid "+%{extraOptionCount} more" msgstr "" @@ -5053,6 +5056,9 @@ msgstr "" msgid "ContainerRegistry|Copy push command" msgstr "" +msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}" +msgstr "" + msgid "ContainerRegistry|Docker connection error" msgstr "" @@ -9809,6 +9815,9 @@ msgstr "" msgid "Helps reduce request volume for protected paths" msgstr "" +msgid "Here you will find recent merge request activity" +msgstr "" + msgid "Hi %{username}!" msgstr "" @@ -12421,6 +12430,9 @@ msgstr "" msgid "No application_settings found" msgstr "" +msgid "No approvers" +msgstr "" + msgid "No authentication methods configured." msgstr "" @@ -13176,9 +13188,15 @@ msgstr "" msgid "Pagination|Last »" msgstr "" +msgid "Pagination|Next" +msgstr "" + msgid "Pagination|Next ›" msgstr "" +msgid "Pagination|Prev" +msgstr "" + msgid "Pagination|« First" msgstr "" @@ -15947,9 +15965,15 @@ msgstr "" msgid "Restart Terminal" msgstr "" +msgid "Restore group" +msgstr "" + msgid "Restore project" msgstr "" +msgid "Restoring the group will prevent the group, its subgroups and projects from being removed on this date." +msgstr "" + msgid "Restoring the project will prevent the project from being removed on this date and restore people's ability to make changes to it." msgstr "" @@ -18549,6 +18573,9 @@ msgstr "" msgid "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository." msgstr "" +msgid "The contents of this group, its subgroups and projects will be permanently removed after %{deletion_adjourned_period} days on %{date}. After this point, your data cannot be recovered." +msgstr "" + msgid "The current issue" msgstr "" @@ -18611,12 +18638,18 @@ msgstr "" msgid "The group and its projects can only be viewed by members." msgstr "" +msgid "The group can be fully restored" +msgstr "" + msgid "The group has already been shared with this group" msgstr "" msgid "The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}." msgstr "" +msgid "The group will be placed in 'pending removal' state" +msgstr "" + msgid "The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination." msgstr "" @@ -19100,9 +19133,18 @@ msgstr "" msgid "This group does not provide any group Runners yet." msgstr "" +msgid "This group has been scheduled for permanent removal on %{date}" +msgstr "" + msgid "This group, including all subgroups, projects and git repositories, will only be reachable from the specified IP address range. Multiple addresses are supported with comma delimiters.<br>Example: <code>192.168.0.0/24,192.168.1.0/24</code>. %{read_more_link}." msgstr "" +msgid "This group, its subgroups and projects has been scheduled for removal on %{date}." +msgstr "" + +msgid "This group, its subgroups and projects will be removed on %{date} since its parent group '%{parent_group_name}'' has been scheduled for removal." +msgstr "" + msgid "This is a \"Ghost User\", created to hold all issues authored by users that have since been deleted. This user cannot be removed." msgstr "" @@ -19274,6 +19316,9 @@ msgstr "" msgid "This project will be removed on %{date}" msgstr "" +msgid "This project will be removed on %{date} since its parent group '%{parent_group_name}' has been scheduled for removal." +msgstr "" + msgid "This repository" msgstr "" @@ -20286,6 +20331,9 @@ msgstr "" msgid "Uploads" msgstr "" +msgid "Upon performing this action, the contents of this group, its subgroup and projects will be permanently removed after %{deletion_adjourned_period} days on <strong>%{date}</strong>. Until that time:" +msgstr "" + msgid "Upstream" msgstr "" diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb index 8c0c426f689..9dc96080732 100644 --- a/spec/features/profiles/password_spec.rb +++ b/spec/features/profiles/password_spec.rb @@ -63,7 +63,7 @@ describe 'Profile > Password' do visit edit_profile_password_path - expect(page).to have_gitlab_http_status(404) + expect(page).to have_gitlab_http_status(:not_found) end end @@ -73,7 +73,7 @@ describe 'Profile > Password' do it 'renders 404' do visit edit_profile_password_path - expect(page).to have_gitlab_http_status(404) + expect(page).to have_gitlab_http_status(:not_found) end end end diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb index dd51eac9be1..d17588bb7b4 100644 --- a/spec/features/projects/badges/coverage_spec.rb +++ b/spec/features/projects/badges/coverage_spec.rb @@ -54,7 +54,7 @@ describe 'test coverage badge' do it 'user requests test coverage badge image' do show_test_coverage_badge - expect(page).to have_gitlab_http_status(404) + expect(page).to have_gitlab_http_status(:not_found) end end diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb index 89da9d1b996..1a1940f6efb 100644 --- a/spec/features/projects/settings/registry_settings_spec.rb +++ b/spec/features/projects/settings/registry_settings_spec.rb @@ -4,20 +4,26 @@ require 'spec_helper' describe 'Project > Settings > CI/CD > Container registry tag expiration policy', :js do let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace) } + let(:project) { create(:project, namespace: user.namespace, container_registry_enabled: container_registry_enabled) } + let(:container_registry_enabled) { true } + + before do + sign_in(user) + stub_container_registry_config(enabled: true) + stub_feature_flags(registry_retention_policies_settings: true) + end context 'as owner' do before do - sign_in(user) visit project_settings_ci_cd_path(project) end - it 'section is available' do + it 'shows available section' do settings_block = find('#js-registry-policies') expect(settings_block).to have_text 'Container Registry tag expiration policy' end - it 'Save expiration policy submit the form' do + it 'saves expiration policy submit the form' do within '#js-registry-policies' do within '.card-body' do find('#expiration-policy-toggle button:not(.is-disabled)').click @@ -34,4 +40,38 @@ describe 'Project > Settings > CI/CD > Container registry tag expiration policy' expect(toast).to have_content('Expiration policy successfully saved.') end end + + context 'when registry is disabled' do + before do + stub_container_registry_config(enabled: false) + visit project_settings_ci_cd_path(project) + end + + it 'does not exists' do + expect(page).not_to have_selector('#js-registry-policies') + end + end + + context 'when container registry is disabled on project' do + let(:container_registry_enabled) { false } + + before do + visit project_settings_ci_cd_path(project) + end + + it 'does not exists' do + expect(page).not_to have_selector('#js-registry-policies') + end + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(registry_retention_policies_settings: false) + visit project_settings_ci_cd_path(project) + end + + it 'does not exists' do + expect(page).not_to have_selector('#js-registry-policies') + end + end end diff --git a/spec/features/projects/tags/user_views_tags_spec.rb b/spec/features/projects/tags/user_views_tags_spec.rb index bc570f502bf..7b49b0d0f65 100644 --- a/spec/features/projects/tags/user_views_tags_spec.rb +++ b/spec/features/projects/tags/user_views_tags_spec.rb @@ -7,7 +7,7 @@ describe 'User views tags', :feature do it do visit project_tags_path(project, format: :atom) - expect(page).to have_gitlab_http_status(200) + expect(page).to have_gitlab_http_status(:ok) end end diff --git a/spec/frontend/graphql_shared/utils_spec.js b/spec/frontend/graphql_shared/utils_spec.js new file mode 100644 index 00000000000..52386bf6ede --- /dev/null +++ b/spec/frontend/graphql_shared/utils_spec.js @@ -0,0 +1,42 @@ +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; + +describe('getIdFromGraphQLId', () => { + [ + { + input: '', + output: null, + }, + { + input: null, + output: null, + }, + { + input: 'gid://', + output: null, + }, + { + input: 'gid://gitlab/', + output: null, + }, + { + input: 'gid://gitlab/Environments', + output: null, + }, + { + input: 'gid://gitlab/Environments/', + output: null, + }, + { + input: 'gid://gitlab/Environments/123', + output: 123, + }, + { + input: 'gid://gitlab/DesignManagement::Version/2', + output: 2, + }, + ].forEach(({ input, output }) => { + it(`getIdFromGraphQLId returns ${output} when passed ${input}`, () => { + expect(getIdFromGraphQLId(input)).toBe(output); + }); + }); +}); diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js index ada3e44f0a4..e3357394551 100644 --- a/spec/frontend/monitoring/mock_data.js +++ b/spec/frontend/monitoring/mock_data.js @@ -332,7 +332,7 @@ export const mockedQueryResultPayloadCoresTotal = { }; const extraEnvironmentData = new Array(15).fill(null).map((_, idx) => ({ - id: 136 + idx, + id: `gid://gitlab/Environments/${150 + idx}`, name: `no-deployment/noop-branch-${idx}`, state: 'available', created_at: '2018-07-04T18:39:41.702Z', @@ -341,7 +341,7 @@ const extraEnvironmentData = new Array(15).fill(null).map((_, idx) => ({ export const environmentData = [ { - id: 34, + id: 'gid://gitlab/Environments/34', name: 'production', state: 'available', external_url: 'http://root-autodevops-deploy.my-fake-domain.com', @@ -359,7 +359,7 @@ export const environmentData = [ }, }, { - id: 35, + id: 'gid://gitlab/Environments/35', name: 'review/noop-branch', state: 'available', external_url: 'http://root-autodevops-deploy-review-noop-branc-die93w.my-fake-domain.com', diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js index d479b2eb083..352900f7087 100644 --- a/spec/frontend/monitoring/store/actions_spec.js +++ b/spec/frontend/monitoring/store/actions_spec.js @@ -20,6 +20,7 @@ import { setGettingStartedEmptyState, duplicateSystemDashboard, } from '~/monitoring/stores/actions'; +import { gqClient, parseEnvironmentsResponse } from '~/monitoring/stores/utils'; import storeState from '~/monitoring/stores/state'; import { deploymentData, @@ -105,37 +106,46 @@ describe('Monitoring store actions', () => { }); }); describe('fetchEnvironmentsData', () => { - it('commits RECEIVE_ENVIRONMENTS_DATA_SUCCESS on error', done => { + it('commits RECEIVE_ENVIRONMENTS_DATA_SUCCESS on error', () => { const dispatch = jest.fn(); const { state } = store; - state.environmentsEndpoint = '/success'; - mock.onGet(state.environmentsEndpoint).reply(200, { - environments: environmentData, - }); - fetchEnvironmentsData({ + state.projectPath = '/gitlab-org/gitlab-test'; + + jest.spyOn(gqClient, 'mutate').mockReturnValue( + Promise.resolve({ + data: { + project: { + data: { + environments: environmentData, + }, + }, + }, + }), + ); + + return fetchEnvironmentsData({ state, dispatch, - }) - .then(() => { - expect(dispatch).toHaveBeenCalledWith('receiveEnvironmentsDataSuccess', environmentData); - done(); - }) - .catch(done.fail); + }).then(() => { + expect(dispatch).toHaveBeenCalledWith( + 'receiveEnvironmentsDataSuccess', + parseEnvironmentsResponse(environmentData, state.projectPath), + ); + }); }); - it('commits RECEIVE_ENVIRONMENTS_DATA_FAILURE on error', done => { + + it('commits RECEIVE_ENVIRONMENTS_DATA_FAILURE on error', () => { const dispatch = jest.fn(); const { state } = store; - state.environmentsEndpoint = '/error'; - mock.onGet(state.environmentsEndpoint).reply(500); - fetchEnvironmentsData({ + state.projectPath = '/gitlab-org/gitlab-test'; + jest.spyOn(gqClient, 'mutate').mockReturnValue(Promise.reject()); + + return fetchEnvironmentsData({ state, dispatch, - }) - .then(() => { - expect(dispatch).toHaveBeenCalledWith('receiveEnvironmentsDataFailure'); - done(); - }) - .catch(done.fail); + }).then(() => { + expect(dispatch).toHaveBeenCalledWith('receiveEnvironmentsDataFailure'); + }); }); }); describe('Set endpoints', () => { diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js index d562aaaefe9..d322d45457e 100644 --- a/spec/frontend/monitoring/store/utils_spec.js +++ b/spec/frontend/monitoring/store/utils_spec.js @@ -1,4 +1,11 @@ -import { normalizeMetric, uniqMetricsId } from '~/monitoring/stores/utils'; +import { + normalizeMetric, + uniqMetricsId, + parseEnvironmentsResponse, + removeLeadingSlash, +} from '~/monitoring/stores/utils'; + +const projectPath = 'gitlab-org/gitlab-test'; describe('normalizeMetric', () => { [ @@ -32,3 +39,71 @@ describe('uniqMetricsId', () => { }); }); }); + +describe('parseEnvironmentsResponse', () => { + [ + { + input: null, + output: [], + }, + { + input: undefined, + output: [], + }, + { + input: [], + output: [], + }, + { + input: [ + { + id: '1', + name: 'env-1', + }, + ], + output: [ + { + id: 1, + name: 'env-1', + metrics_path: `${projectPath}/environments/1/metrics`, + }, + ], + }, + { + input: [ + { + id: 'gid://gitlab/Environment/12', + name: 'env-12', + }, + ], + output: [ + { + id: 12, + name: 'env-12', + metrics_path: `${projectPath}/environments/12/metrics`, + }, + ], + }, + ].forEach(({ input, output }) => { + it(`parseEnvironmentsResponse returns ${JSON.stringify(output)} with input ${JSON.stringify( + input, + )}`, () => { + expect(parseEnvironmentsResponse(input, projectPath)).toEqual(output); + }); + }); +}); + +describe('removeLeadingSlash', () => { + [ + { input: null, output: '' }, + { input: '', output: '' }, + { input: 'gitlab-org', output: 'gitlab-org' }, + { input: 'gitlab-org/gitlab', output: 'gitlab-org/gitlab' }, + { input: '/gitlab-org/gitlab', output: 'gitlab-org/gitlab' }, + { input: '////gitlab-org/gitlab', output: 'gitlab-org/gitlab' }, + ].forEach(({ input, output }) => { + it(`removeLeadingSlash returns ${output} with input ${input}`, () => { + expect(removeLeadingSlash(input)).toEqual(output); + }); + }); +}); diff --git a/spec/frontend/registry/settings/components/registry_settings_app_spec.js b/spec/frontend/registry/settings/components/registry_settings_app_spec.js index eceb5bf643c..8a5c5d84198 100644 --- a/spec/frontend/registry/settings/components/registry_settings_app_spec.js +++ b/spec/frontend/registry/settings/components/registry_settings_app_spec.js @@ -1,16 +1,21 @@ import { shallowMount } from '@vue/test-utils'; +import { GlAlert } from '@gitlab/ui'; import component from '~/registry/settings/components/registry_settings_app.vue'; +import SettingsForm from '~/registry/settings/components/settings_form.vue'; import { createStore } from '~/registry/settings/store/'; +import { SET_IS_DISABLED } from '~/registry/settings/store/mutation_types'; import { FETCH_SETTINGS_ERROR_MESSAGE } from '~/registry/settings/constants'; describe('Registry Settings App', () => { let wrapper; let store; - const findSettingsComponent = () => wrapper.find({ ref: 'settings-form' }); + const findSettingsComponent = () => wrapper.find(SettingsForm); + const findAlert = () => wrapper.find(GlAlert); - const mountComponent = ({ dispatchMock } = {}) => { + const mountComponent = ({ dispatchMock = 'mockResolvedValue', isDisabled = false } = {}) => { store = createStore(); + store.commit(SET_IS_DISABLED, isDisabled); const dispatchSpy = jest.spyOn(store, 'dispatch'); if (dispatchMock) { dispatchSpy[dispatchMock](); @@ -30,12 +35,12 @@ describe('Registry Settings App', () => { }); it('renders', () => { - mountComponent({ dispatchMock: 'mockResolvedValue' }); + mountComponent(); expect(wrapper.element).toMatchSnapshot(); }); it('call the store function to load the data on mount', () => { - mountComponent({ dispatchMock: 'mockResolvedValue' }); + mountComponent(); expect(store.dispatch).toHaveBeenCalledWith('fetchSettings'); }); @@ -49,7 +54,21 @@ describe('Registry Settings App', () => { }); it('renders the setting form', () => { - mountComponent({ dispatchMock: 'mockResolvedValue' }); + mountComponent(); expect(findSettingsComponent().exists()).toBe(true); }); + + describe('isDisabled', () => { + beforeEach(() => { + mountComponent({ isDisabled: true }); + }); + + it('the form is hidden', () => { + expect(findSettingsComponent().exists()).toBe(false); + }); + + it('shows an alert', () => { + expect(findAlert().exists()).toBe(true); + }); + }); }); diff --git a/spec/frontend/registry/settings/store/actions_spec.js b/spec/frontend/registry/settings/store/actions_spec.js index 65b1fc42bfe..f904d0b660a 100644 --- a/spec/frontend/registry/settings/store/actions_spec.js +++ b/spec/frontend/registry/settings/store/actions_spec.js @@ -5,18 +5,38 @@ import * as types from '~/registry/settings/store/mutation_types'; describe('Actions Registry Store', () => { describe.each` - actionName | mutationName | payload - ${'setInitialState'} | ${types.SET_INITIAL_STATE} | ${'foo'} - ${'updateSettings'} | ${types.UPDATE_SETTINGS} | ${'foo'} - ${'receiveSettingsSuccess'} | ${types.SET_SETTINGS} | ${'foo'} - ${'toggleLoading'} | ${types.TOGGLE_LOADING} | ${undefined} - ${'resetSettings'} | ${types.RESET_SETTINGS} | ${undefined} + actionName | mutationName | payload + ${'setInitialState'} | ${types.SET_INITIAL_STATE} | ${'foo'} + ${'updateSettings'} | ${types.UPDATE_SETTINGS} | ${'foo'} + ${'toggleLoading'} | ${types.TOGGLE_LOADING} | ${undefined} + ${'resetSettings'} | ${types.RESET_SETTINGS} | ${undefined} `('%s action invokes %s mutation with payload %s', ({ actionName, mutationName, payload }) => { it('should set the initial state', done => { testAction(actions[actionName], payload, {}, [{ type: mutationName, payload }], [], done); }); }); + describe('receiveSettingsSuccess', () => { + it('calls SET_SETTINGS when data is present', () => { + testAction( + actions.receiveSettingsSuccess, + 'foo', + {}, + [{ type: types.SET_SETTINGS, payload: 'foo' }], + [], + ); + }); + it('calls SET_IS_DISABLED when data is not present', () => { + testAction( + actions.receiveSettingsSuccess, + null, + {}, + [{ type: types.SET_IS_DISABLED, payload: true }], + [], + ); + }); + }); + describe('fetchSettings', () => { const state = { projectId: 'bar', diff --git a/spec/frontend/registry/settings/store/mutations_spec.js b/spec/frontend/registry/settings/store/mutations_spec.js index 1a0effbe125..deb59089d60 100644 --- a/spec/frontend/registry/settings/store/mutations_spec.js +++ b/spec/frontend/registry/settings/store/mutations_spec.js @@ -32,6 +32,7 @@ describe('Mutations Registry Store', () => { expect(mockState.settings).toEqual(expectedState.settings); }); }); + describe('SET_SETTINGS', () => { it('should set the settings and original', () => { const payload = { foo: 'baz' }; @@ -41,6 +42,7 @@ describe('Mutations Registry Store', () => { expect(mockState.original).toEqual(expectedState.settings); }); }); + describe('RESET_SETTINGS', () => { it('should copy original over settings', () => { mockState.settings = { foo: 'bar' }; @@ -49,10 +51,18 @@ describe('Mutations Registry Store', () => { expect(mockState.settings).toEqual(mockState.original); }); }); + describe('TOGGLE_LOADING', () => { it('should toggle the loading', () => { mutations[types.TOGGLE_LOADING](mockState); expect(mockState.isLoading).toEqual(true); }); }); + + describe('SET_IS_DISABLED', () => { + it('should set isDisabled', () => { + mutations[types.SET_IS_DISABLED](mockState, true); + expect(mockState.isDisabled).toEqual(true); + }); + }); }); diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js index 39575ee9f97..2889d7ae4ff 100644 --- a/spec/javascripts/groups/components/group_item_spec.js +++ b/spec/javascripts/groups/components/group_item_spec.js @@ -155,6 +155,35 @@ describe('GroupItemComponent', () => { }); describe('template', () => { + let group = null; + + describe('for a group pending deletion', () => { + beforeEach(() => { + group = { ...mockParentGroupItem, pendingRemoval: true }; + vm = createComponent(group); + }); + + it('renders the group pending removal badge', () => { + const badgeEl = vm.$el.querySelector('.badge-warning'); + + expect(badgeEl).toBeDefined(); + expect(badgeEl).toContainText('pending removal'); + }); + }); + + describe('for a group not scheduled for deletion', () => { + beforeEach(() => { + group = { ...mockParentGroupItem, pendingRemoval: false }; + vm = createComponent(group); + }); + + it('does not render the group pending removal badge', () => { + const groupTextContainer = vm.$el.querySelector('.group-text-container'); + + expect(groupTextContainer).not.toContainText('pending removal'); + }); + }); + it('should render component template correctly', () => { const visibilityIconEl = vm.$el.querySelector('.item-visibility'); diff --git a/spec/lib/gitlab/quick_actions/extractor_spec.rb b/spec/lib/gitlab/quick_actions/extractor_spec.rb index f1acb5b7049..2536e4a372b 100644 --- a/spec/lib/gitlab/quick_actions/extractor_spec.rb +++ b/spec/lib/gitlab/quick_actions/extractor_spec.rb @@ -294,4 +294,22 @@ describe Gitlab::QuickActions::Extractor do expect(msg).to eq expected_msg end end + + describe '#redact_commands' do + using RSpec::Parameterized::TableSyntax + + where(:text, :expected) do + "hello\n/labels ~label1 ~label2\nworld" | "hello\n`/labels ~label1 ~label2`\nworld" + "hello\n/open\n/labels ~label1\nworld" | "hello\n`/open`\n`/labels ~label1`\nworld" + "hello\n/reopen\nworld" | "hello\n`/reopen`\nworld" + "/reopen\nworld" | "`/reopen`\nworld" + "hello\n/open" | "hello\n`/open`" + end + + with_them do + it 'encloses quick actions with code span markdown' do + expect(extractor.redact_commands(text)).to eq(expected) + end + end + end end |