Commit c282dba8 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 2c2dd5e3
......@@ -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
/**
* 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 {};
<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"
>
......
......@@ -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,
};
}
......
query getEnvironments($projectPath: ID!, $search: String) {
project(fullPath: $projectPath) {
data: environments(search: $search) {
environments: nodes {
name
id
}
}
}
}
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
......
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.
......
<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>
......@@ -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 }) => {
......
......@@ -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';
......@@ -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 };
},
......
......@@ -7,6 +7,10 @@ export default () => ({
* Boolean to determine if the UI is loading data from the API
*/
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:
......
......@@ -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; }
......
......@@ -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
......
......@@ -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
......@@ -73,3 +73,5 @@ module LoadedInGroupList
@member_count ||= try(:preloaded_member_count) || users.count
end
end
LoadedInGroupList::ClassMethods.prepend_if_ee('EE::LoadedInGroupList::ClassMethods')
......@@ -467,6 +467,10 @@ class Group < Namespace
import_export_upload&.export_file
end
def adjourned_deletion?
false
end
private
def update_two_factor_requirement
......
......@@ -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')
......
......@@ -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
.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) }
- if group.adjourned_deletion?
= render_if_exists 'groups/settings/adjourned_deletion', group: group
- else
= render 'groups/settings/permanent_deletion', group: group
......@@ -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
......
-# 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'
-# 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'}
......@@ -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)
......@@ -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' }
......@@ -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
......@@ -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')
......@@ -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)
......@@ -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
......
......@@ -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
......
---
title: Update Kaminari templates to match gl-pagination's markup
merge_request: 23582
author:
type: other
---
title: Add UI for 'soft-delete for groups' feature
merge_request: 19483
author:
type: added
......@@ -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,
......
......@@ -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
......
......@@ -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).
......
......@@ -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`
......
......@@ -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|
......@@ -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
......
......@@ -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
......
......@@ -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.
......@@ -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'
```
# 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
```
......@@ -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
......
......@@ -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
......
......@@ -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 ""
......
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -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
......
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);
});
});
});
......@@ -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',
......
......@@ -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', () => {
......
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);
});
});
});
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);
});
});
});
......@@ -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',
......
......@@ -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);
});
});
});
......@@ -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');
......
......@@ -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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment