summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-01-24 15:09:00 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-01-24 15:09:00 +0000
commitc282dba898a4cb0645f88579339502a4e3778727 (patch)
tree94a6457ce4438e085c9ae43bc51a2b5a29787bf2
parent2c2dd5e36c4ed5f09f488be288882d98f9124d12 (diff)
downloadgitlab-ce-c282dba898a4cb0645f88579339502a4e3778727.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop.yml3
-rw-r--r--app/assets/javascripts/graphql_shared/utils.js12
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue9
-rw-r--r--app/assets/javascripts/groups/store/groups_store.js2
-rw-r--r--app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql10
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js28
-rw-r--r--app/assets/javascripts/monitoring/stores/utils.js33
-rw-r--r--app/assets/javascripts/registry/settings/components/registry_settings_app.vue27
-rw-r--r--app/assets/javascripts/registry/settings/store/actions.js8
-rw-r--r--app/assets/javascripts/registry/settings/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/registry/settings/store/mutations.js3
-rw-r--r--app/assets/javascripts/registry/settings/store/state.js4
-rw-r--r--app/assets/stylesheets/utilities.scss1
-rw-r--r--app/controllers/admin/groups_controller.rb7
-rw-r--r--app/helpers/projects_helper.rb6
-rw-r--r--app/models/concerns/loaded_in_group_list.rb2
-rw-r--r--app/models/group.rb4
-rw-r--r--app/views/admin/groups/_group.html.haml1
-rw-r--r--app/views/groups/settings/_advanced.html.haml11
-rw-r--r--app/views/groups/settings/_permanent_deletion.html.haml9
-rw-r--r--app/views/groups/settings/_remove.html.haml5
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_first_page.html.haml9
-rw-r--r--app/views/kaminari/gitlab/_last_page.html.haml9
-rw-r--r--app/views/kaminari/gitlab/_next_page.html.haml4
-rw-r--r--app/views/kaminari/gitlab/_page.html.haml6
-rw-r--r--app/views/kaminari/gitlab/_paginator.html.haml6
-rw-r--r--app/views/kaminari/gitlab/_prev_page.html.haml4
-rw-r--r--app/views/kaminari/gitlab/_without_count.html.haml8
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml4
-rw-r--r--app/views/projects/show.html.haml1
-rw-r--r--changelogs/unreleased/33071-update-kaminari-templates.yml5
-rw-r--r--changelogs/unreleased/33257-prevent-accidental-deletions-via-soft-delete-for-groups-ui-changes.yml5
-rw-r--r--doc/administration/gitaly/index.md6
-rw-r--r--doc/api/groups.md2
-rw-r--r--doc/api/projects.md2
-rw-r--r--doc/development/documentation/site_architecture/release_process.md38
-rw-r--r--doc/development/permissions.md39
-rw-r--r--doc/user/group/index.md24
-rw-r--r--doc/user/packages/container_registry/img/expiration-policy-app.pngbin0 -> 90268 bytes
-rw-r--r--doc/user/packages/container_registry/index.md41
-rw-r--r--doc/user/packages/index.md3
-rw-r--r--doc/user/packages/maven_repository/index.md50
-rw-r--r--doc/user/packages/workflows/monorepo.md119
-rw-r--r--doc/user/project/settings/index.md25
-rw-r--r--lib/gitlab/quick_actions/extractor.rb53
-rw-r--r--locale/gitlab.pot48
-rw-r--r--spec/features/profiles/password_spec.rb4
-rw-r--r--spec/features/projects/badges/coverage_spec.rb2
-rw-r--r--spec/features/projects/settings/registry_settings_spec.rb48
-rw-r--r--spec/features/projects/tags/user_views_tags_spec.rb2
-rw-r--r--spec/frontend/graphql_shared/utils_spec.js42
-rw-r--r--spec/frontend/monitoring/mock_data.js6
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js54
-rw-r--r--spec/frontend/monitoring/store/utils_spec.js77
-rw-r--r--spec/frontend/registry/settings/components/registry_settings_app_spec.js29
-rw-r--r--spec/frontend/registry/settings/store/actions_spec.js32
-rw-r--r--spec/frontend/registry/settings/store/mutations_spec.js10
-rw-r--r--spec/javascripts/groups/components/group_item_spec.js29
-rw-r--r--spec/lib/gitlab/quick_actions/extractor_spec.rb18
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
new file mode 100644
index 00000000000..38de4d85ae5
--- /dev/null
+++ b/doc/user/packages/container_registry/img/expiration-policy-app.png
Binary files differ
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