summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-09-04 03:08:22 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-09-04 03:08:22 +0000
commit583bde3f83951fa4c294804edc2e9c57fb293733 (patch)
tree83900919e93ea9c1dab7571c9d4e02e73d8b8fb5
parentdc965b8cc88f8dadf879c0d80214864c699ebf1f (diff)
downloadgitlab-ce-583bde3f83951fa4c294804edc2e9c57fb293733.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/groups/components/invite_members_banner.vue42
-rw-r--r--app/assets/javascripts/groups/init_invite_members_banner.js21
-rw-r--r--app/assets/javascripts/pages/groups/shared/group_details.js2
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue30
-rw-r--r--app/assets/javascripts/snippets/components/snippet_visibility_edit.vue50
-rw-r--r--app/assets/javascripts/snippets/constants.js12
-rw-r--r--app/assets/javascripts/snippets/index.js13
-rw-r--r--app/assets/javascripts/snippets/queries/snippet_visibility.query.graphql5
-rw-r--r--app/assets/javascripts/snippets/utils/blob.js15
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb6
-rw-r--r--app/helpers/groups_helper.rb15
-rw-r--r--app/views/groups/show.html.haml6
-rw-r--r--app/views/layouts/_page.html.haml1
-rw-r--r--app/views/shared/snippets/_form.html.haml3
-rw-r--r--changelogs/unreleased/cat-time-precision-2fa-ldap.yml5
-rw-r--r--config/feature_flags/development/invite_your_teammates_banner_a.yml7
-rw-r--r--doc/.vale/gitlab/RelativeLinks.yml2
-rw-r--r--doc/api/pages_domains.md34
-rw-r--r--doc/ci/environments/incremental_rollouts.md2
-rw-r--r--doc/ci/examples/README.md1
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/install/installation.md4
-rw-r--r--doc/install/requirements.md4
-rw-r--r--doc/user/project/pages/getting_started/pages_from_scratch.md2
-rw-r--r--lib/tasks/gettext.rake4
-rw-r--r--locale/gitlab.pot9
-rwxr-xr-xscripts/lint-doc.sh2
-rw-r--r--spec/frontend/groups/components/invite_members_banner_spec.js76
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap8
-rw-r--r--spec/frontend/snippets/components/edit_spec.js7
-rw-r--r--spec/frontend/snippets/components/snippet_visibility_edit_spec.js122
-rw-r--r--spec/helpers/groups_helper_spec.rb44
32 files changed, 334 insertions, 222 deletions
diff --git a/app/assets/javascripts/groups/components/invite_members_banner.vue b/app/assets/javascripts/groups/components/invite_members_banner.vue
new file mode 100644
index 00000000000..83a23134279
--- /dev/null
+++ b/app/assets/javascripts/groups/components/invite_members_banner.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlBanner } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlBanner,
+ },
+ inject: ['svgPath', 'inviteMembersPath'],
+ data() {
+ return {
+ visible: true,
+ };
+ },
+ methods: {
+ handleClose() {
+ this.visible = false;
+ },
+ },
+ i18n: {
+ title: s__('InviteMembersBanner|Collaborate with your team'),
+ body: s__(
+ "InviteMembersBanner|We noticed that you haven't invited anyone to this group. Invite your colleagues so you can discuss issues, collaborate on merge requests, and share your knowledge.",
+ ),
+ button_text: s__('InviteMembersBanner|Invite your colleagues'),
+ },
+};
+</script>
+
+<template>
+ <gl-banner
+ v-if="visible"
+ ref="banner"
+ :title="$options.i18n.title"
+ :button-text="$options.i18n.button_text"
+ :svg-path="svgPath"
+ :button-link="inviteMembersPath"
+ @close="handleClose"
+ >
+ <p>{{ $options.i18n.body }}</p>
+ </gl-banner>
+</template>
diff --git a/app/assets/javascripts/groups/init_invite_members_banner.js b/app/assets/javascripts/groups/init_invite_members_banner.js
new file mode 100644
index 00000000000..dbc3ed65a4f
--- /dev/null
+++ b/app/assets/javascripts/groups/init_invite_members_banner.js
@@ -0,0 +1,21 @@
+import Vue from 'vue';
+import InviteMembersBanner from '~/groups/components/invite_members_banner.vue';
+
+export default function initInviteMembersBanner() {
+ const el = document.querySelector('.js-group-invite-members-banner');
+
+ if (!el) {
+ return false;
+ }
+
+ const { svgPath, inviteMembersPath } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ svgPath,
+ inviteMembersPath,
+ },
+ render: createElement => createElement(InviteMembersBanner),
+ });
+}
diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js
index 37b253d7c48..8546b1f759f 100644
--- a/app/assets/javascripts/pages/groups/shared/group_details.js
+++ b/app/assets/javascripts/pages/groups/shared/group_details.js
@@ -8,6 +8,7 @@ import NotificationsForm from '~/notifications_form';
import ProjectsList from '~/projects_list';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GroupTabs from './group_tabs';
+import initInviteMembersBanner from '~/groups/init_invite_members_banner';
export default function initGroupDetails(actionName = 'show') {
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
@@ -27,4 +28,5 @@ export default function initGroupDetails(actionName = 'show') {
if (newGroupChildWrapper) {
new NewGroupChild(newGroupChildWrapper);
}
+ initInviteMembersBanner();
}
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue
index 1a539aa0876..3d2eaebf1cb 100644
--- a/app/assets/javascripts/snippets/components/edit.vue
+++ b/app/assets/javascripts/snippets/components/edit.vue
@@ -6,21 +6,19 @@ import { __, sprintf } from '~/locale';
import TitleField from '~/vue_shared/components/form/title.vue';
import { redirectTo, joinPaths } from '~/lib/utils/url_utility';
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
-import { SNIPPET_MARK_EDIT_APP_START } from '~/performance_constants';
import UpdateSnippetMutation from '../mutations/updateSnippet.mutation.graphql';
import CreateSnippetMutation from '../mutations/createSnippet.mutation.graphql';
import { getSnippetMixin } from '../mixins/snippets';
import {
+ SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_CREATE_MUTATION_ERROR,
SNIPPET_UPDATE_MUTATION_ERROR,
- SNIPPET_VISIBILITY_PRIVATE,
} from '../constants';
-import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql';
-
import SnippetBlobActionsEdit from './snippet_blob_actions_edit.vue';
import SnippetVisibilityEdit from './snippet_visibility_edit.vue';
import SnippetDescriptionEdit from './snippet_description_edit.vue';
+import { SNIPPET_MARK_EDIT_APP_START } from '~/performance_constants';
export default {
components: {
@@ -33,15 +31,6 @@ export default {
GlLoadingIcon,
},
mixins: [getSnippetMixin],
- apollo: {
- defaultVisibility: {
- query: defaultVisibilityQuery,
- manual: true,
- result({ data: { selectedLevel } }) {
- this.selectedLevelDefault = selectedLevel;
- },
- },
- },
props: {
markdownPreviewPath: {
type: String,
@@ -67,7 +56,6 @@ export default {
isUpdating: false,
newSnippet: false,
actions: [],
- selectedLevelDefault: SNIPPET_VISIBILITY_PRIVATE,
};
},
computed: {
@@ -110,13 +98,6 @@ export default {
descriptionFieldId() {
return `${this.isProjectSnippet ? 'project' : 'personal'}_snippet_description`;
},
- newSnippetSchema() {
- return {
- title: '',
- description: '',
- visibilityLevel: this.selectedLevelDefault,
- };
- },
},
beforeCreate() {
performance.mark(SNIPPET_MARK_EDIT_APP_START);
@@ -145,7 +126,7 @@ export default {
},
onNewSnippetFetched() {
this.newSnippet = true;
- this.snippet = this.newSnippetSchema;
+ this.snippet = this.$options.newSnippetSchema;
},
onExistingSnippetFetched() {
this.newSnippet = false;
@@ -203,6 +184,11 @@ export default {
this.actions = actions;
},
},
+ newSnippetSchema: {
+ title: '',
+ description: '',
+ visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
+ },
};
</script>
<template>
diff --git a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
index 25ad7c214b2..299bb8fcfad 100644
--- a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
@@ -1,8 +1,11 @@
<script>
import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
-import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql';
-import { defaultSnippetVisibilityLevels } from '../utils/blob';
-import { SNIPPET_LEVELS_RESTRICTED, SNIPPET_LEVELS_DISABLED } from '~/snippets/constants';
+import {
+ SNIPPET_VISIBILITY,
+ SNIPPET_VISIBILITY_PRIVATE,
+ SNIPPET_VISIBILITY_INTERNAL,
+ SNIPPET_VISIBILITY_PUBLIC,
+} from '~/snippets/constants';
export default {
components: {
@@ -12,16 +15,6 @@ export default {
GlFormRadioGroup,
GlLink,
},
- apollo: {
- defaultVisibility: {
- query: defaultVisibilityQuery,
- manual: true,
- result({ data: { visibilityLevels, multipleLevelsRestricted } }) {
- this.visibilityLevels = defaultSnippetVisibilityLevels(visibilityLevels);
- this.multipleLevelsRestricted = multipleLevelsRestricted;
- },
- },
- },
props: {
helpLink: {
type: String,
@@ -35,17 +28,19 @@ export default {
},
value: {
type: String,
- required: true,
+ required: false,
+ default: SNIPPET_VISIBILITY_PRIVATE,
},
},
- data() {
- return {
- visibilityLevels: [],
- multipleLevelsRestricted: false,
- };
+ computed: {
+ visibilityOptions() {
+ return [
+ SNIPPET_VISIBILITY_PRIVATE,
+ SNIPPET_VISIBILITY_INTERNAL,
+ SNIPPET_VISIBILITY_PUBLIC,
+ ].map(key => ({ value: key, ...SNIPPET_VISIBILITY[key] }));
+ },
},
- SNIPPET_LEVELS_DISABLED,
- SNIPPET_LEVELS_RESTRICTED,
};
</script>
<template>
@@ -56,10 +51,10 @@ export default {
><gl-icon :size="12" name="question"
/></gl-link>
</label>
- <gl-form-group id="visibility-level-setting" class="gl-mb-0">
- <gl-form-radio-group :checked="value" stacked v-bind="$attrs" v-on="$listeners">
+ <gl-form-group id="visibility-level-setting">
+ <gl-form-radio-group v-bind="$attrs" :checked="value" stacked v-on="$listeners">
<gl-form-radio
- v-for="option in visibilityLevels"
+ v-for="option in visibilityOptions"
:key="option.value"
:value="option.value"
class="mb-3"
@@ -76,12 +71,5 @@ export default {
</gl-form-radio>
</gl-form-radio-group>
</gl-form-group>
-
- <div class="text-muted" data-testid="restricted-levels-info">
- <template v-if="!visibilityLevels.length">{{ $options.SNIPPET_LEVELS_DISABLED }}</template>
- <template v-else-if="multipleLevelsRestricted">{{
- $options.SNIPPET_LEVELS_RESTRICTED
- }}</template>
- </div>
</div>
</template>
diff --git a/app/assets/javascripts/snippets/constants.js b/app/assets/javascripts/snippets/constants.js
index e75922df15f..12b83525bf7 100644
--- a/app/assets/javascripts/snippets/constants.js
+++ b/app/assets/javascripts/snippets/constants.js
@@ -33,15 +33,3 @@ export const SNIPPET_BLOB_ACTION_MOVE = 'move';
export const SNIPPET_BLOB_ACTION_DELETE = 'delete';
export const SNIPPET_MAX_BLOBS = 10;
-
-export const SNIPPET_LEVELS_MAP = {
- 0: SNIPPET_VISIBILITY_PRIVATE,
- 10: SNIPPET_VISIBILITY_INTERNAL,
- 20: SNIPPET_VISIBILITY_PUBLIC,
-};
-export const SNIPPET_LEVELS_RESTRICTED = __(
- 'Other visibility settings have been disabled by the administrator.',
-);
-export const SNIPPET_LEVELS_DISABLED = __(
- 'Visibility settings have been disabled by the administrator.',
-);
diff --git a/app/assets/javascripts/snippets/index.js b/app/assets/javascripts/snippets/index.js
index f0f7ddac47f..bb5e7d6e3f0 100644
--- a/app/assets/javascripts/snippets/index.js
+++ b/app/assets/javascripts/snippets/index.js
@@ -5,7 +5,6 @@ import createDefaultClient from '~/lib/graphql';
import SnippetsShow from './components/show.vue';
import SnippetsEdit from './components/edit.vue';
-import { SNIPPET_LEVELS_MAP, SNIPPET_VISIBILITY_PRIVATE } from '~/snippets/constants';
Vue.use(VueApollo);
Vue.use(Translate);
@@ -19,23 +18,13 @@ function appFactory(el, Component) {
defaultClient: createDefaultClient(),
});
- const { visibilityLevels, selectedLevel, multipleLevelsRestricted, ...restDataset } = el.dataset;
-
- apolloProvider.clients.defaultClient.cache.writeData({
- data: {
- visibilityLevels: JSON.parse(visibilityLevels),
- selectedLevel: SNIPPET_LEVELS_MAP[selectedLevel] ?? SNIPPET_VISIBILITY_PRIVATE,
- multipleLevelsRestricted: 'multipleLevelsRestricted' in el.dataset,
- },
- });
-
return new Vue({
el,
apolloProvider,
render(createElement) {
return createElement(Component, {
props: {
- ...restDataset,
+ ...el.dataset,
},
});
},
diff --git a/app/assets/javascripts/snippets/queries/snippet_visibility.query.graphql b/app/assets/javascripts/snippets/queries/snippet_visibility.query.graphql
deleted file mode 100644
index 5bd6c131bab..00000000000
--- a/app/assets/javascripts/snippets/queries/snippet_visibility.query.graphql
+++ /dev/null
@@ -1,5 +0,0 @@
-query defaultSnippetVisibility {
- visibilityLevels @client
- selectedLevel @client
- multipleLevelsRestricted @client
-}
diff --git a/app/assets/javascripts/snippets/utils/blob.js b/app/assets/javascripts/snippets/utils/blob.js
index 21f52671801..fd5ff9a3d2e 100644
--- a/app/assets/javascripts/snippets/utils/blob.js
+++ b/app/assets/javascripts/snippets/utils/blob.js
@@ -4,8 +4,6 @@ import {
SNIPPET_BLOB_ACTION_UPDATE,
SNIPPET_BLOB_ACTION_MOVE,
SNIPPET_BLOB_ACTION_DELETE,
- SNIPPET_LEVELS_MAP,
- SNIPPET_VISIBILITY,
} from '../constants';
const createLocalId = () => uniqueId('blob_local_');
@@ -66,16 +64,3 @@ export const diffAll = (blobs, origBlobs) => {
return [...deletedEntries, ...newEntries];
};
-
-export const defaultSnippetVisibilityLevels = arr => {
- if (Array.isArray(arr)) {
- return arr.map(l => {
- const translatedLevel = SNIPPET_LEVELS_MAP[l];
- return {
- value: translatedLevel,
- ...SNIPPET_VISIBILITY[translatedLevel],
- };
- });
- }
- return [];
-};
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index 2cc51c65c26..b93c98a4790 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -129,6 +129,10 @@ module AuthenticatesWithTwoFactor
def user_changed?(user)
return false unless session[:user_updated_at]
- user.updated_at != session[:user_updated_at]
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/244638
+ # Rounding errors happen when the user is updated, as the Rails ActiveRecord
+ # object has higher precision than what is stored in the database, therefore
+ # using .to_i to force truncation to the timestamp
+ user.updated_at.to_i != session[:user_updated_at].to_i
end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index eb80acd869f..06a52457fd6 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -167,8 +167,23 @@ module GroupsHelper
@group.packages_feature_enabled?
end
+ def show_invite_banner?(group)
+ Feature.enabled?(:invite_your_teammates_banner_a, group) &&
+ can?(current_user, :admin_group, group) &&
+ !just_created? &&
+ !multiple_members?(group)
+ end
+
private
+ def just_created?
+ flash[:notice] =~ /successfully created/
+ end
+
+ def multiple_members?(group)
+ group.member_count > 1
+ end
+
def get_group_sidebar_links
links = [:overview, :group_members]
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 6ad864121d7..523c18ed3f3 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -2,6 +2,12 @@
- page_title _("Groups")
- @content_class = "limit-container-width" unless fluid_layout
+- if show_invite_banner?(@group)
+ = content_for :group_invite_members_banner do
+ .container-fluid.container-limited{ class: "gl-pb-2! gl-pt-6! #{@content_class}" }
+ .js-group-invite-members-banner{ data: { svg_path: image_path('illustrations/merge_requests.svg'),
+ invite_members_path: group_group_members_path(@group) } }
+
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 1ef368160a8..5184bc93a81 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -3,6 +3,7 @@
= render "layouts/nav/sidebar/#{nav}"
.content-wrapper{ class: "#{@content_wrapper_class}" }
.mobile-overlay
+ = yield :group_invite_members_banner
.alert-wrapper
= render 'shared/outdated_browser'
= render_if_exists "layouts/header/licensed_user_count_threshold"
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 41098b60193..81277b50d13 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -1,6 +1,5 @@
- if Feature.enabled?(:snippets_edit_vue)
- - available_visibility_levels = available_visibility_levels(@snippet)
- #js-snippet-edit.snippet-form{ data: {'project_path': @snippet.project&.full_path, 'snippet-gid': @snippet.new_record? ? '' : @snippet.to_global_id, 'markdown-preview-path': preview_markdown_path(parent), 'markdown-docs-path': help_page_path('user/markdown'), 'visibility-help-link': help_page_path("public_access/public_access"), 'visibility_levels': available_visibility_levels, 'selected_level': snippets_selected_visibility_level(available_visibility_levels, @snippet.visibility_level), 'multiple_levels_restricted': multiple_visibility_levels_restricted? } }
+ #js-snippet-edit.snippet-form{ data: {'project_path': @snippet.project&.full_path, 'snippet-gid': @snippet.new_record? ? '' : @snippet.to_global_id, 'markdown-preview-path': preview_markdown_path(parent), 'markdown-docs-path': help_page_path('user/markdown'), 'visibility-help-link': help_page_path("public_access/public_access") } }
- else
.snippet-form-holder
= form_for @snippet, url: url,
diff --git a/changelogs/unreleased/cat-time-precision-2fa-ldap.yml b/changelogs/unreleased/cat-time-precision-2fa-ldap.yml
new file mode 100644
index 00000000000..dc2cdaa8632
--- /dev/null
+++ b/changelogs/unreleased/cat-time-precision-2fa-ldap.yml
@@ -0,0 +1,5 @@
+---
+title: Update the 2FA user update check to account for rounding errors
+merge_request: 41327
+author:
+type: fixed
diff --git a/config/feature_flags/development/invite_your_teammates_banner_a.yml b/config/feature_flags/development/invite_your_teammates_banner_a.yml
new file mode 100644
index 00000000000..07aec532e16
--- /dev/null
+++ b/config/feature_flags/development/invite_your_teammates_banner_a.yml
@@ -0,0 +1,7 @@
+---
+name: invite_your_teammates_banner_a
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37658
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/231275
+group: group::expansion
+type: development
+default_enabled: false \ No newline at end of file
diff --git a/doc/.vale/gitlab/RelativeLinks.yml b/doc/.vale/gitlab/RelativeLinks.yml
index f7407375b84..7af20d8226f 100644
--- a/doc/.vale/gitlab/RelativeLinks.yml
+++ b/doc/.vale/gitlab/RelativeLinks.yml
@@ -10,4 +10,4 @@ link: https://docs.gitlab.com/ee/development/documentation/styleguide.html#links
level: error
scope: raw
raw:
- - '\[.+\]\(https?:\/\/docs\.gitlab\.com\/ee.*\)'
+ - '\[.+\]\(https?:\/\/docs\.gitlab\.com\/[ce]e.*\)'
diff --git a/doc/api/pages_domains.md b/doc/api/pages_domains.md
index 1fddc79814f..6f7236c8d1a 100644
--- a/doc/api/pages_domains.md
+++ b/doc/api/pages_domains.md
@@ -10,9 +10,9 @@ Endpoints for connecting custom domain(s) and TLS certificates in [GitLab Pages]
The GitLab Pages feature must be enabled to use these endpoints. Find out more about [administering](../administration/pages/index.md) and [using](../user/project/pages/index.md) the feature.
-## List all pages domains
+## List all Pages domains
-Get a list of all pages domains. The user must have admin permissions.
+Get a list of all Pages domains. The user must have admin permissions.
```plaintext
GET /pages/domains
@@ -37,9 +37,9 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
]
```
-## List pages domains
+## List Pages domains
-Get a list of project pages domains. The user must have permissions to view pages domains.
+Get a list of project Pages domains. The user must have permissions to view Pages domains.
```plaintext
GET /projects/:id/pages/domains
@@ -73,9 +73,9 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
]
```
-## Single pages domain
+## Single Pages domain
-Get a single project pages domain. The user must have permissions to view pages domains.
+Get a single project Pages domain. The user must have permissions to view Pages domains.
```plaintext
GET /projects/:id/pages/domains/:domain
@@ -115,9 +115,9 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
}
```
-## Create new pages domain
+## Create new Pages domain
-Creates a new pages domain. The user must have permissions to create new pages domains.
+Creates a new Pages domain. The user must have permissions to create new Pages domains.
```plaintext
POST /projects/:id/pages/domains
@@ -131,14 +131,20 @@ POST /projects/:id/pages/domains
| `certificate` | file/string | no | The certificate in PEM format with intermediates following in most specific to least specific order.|
| `key` | file/string | no | The certificate key in PEM format. |
+Create a new Pages domain with a certificate from a `.pem` file:
+
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "domain=ssl.domain.example" --form "certificate=@/path/to/cert.pem" --form "key=@/path/to/key.pem" "https://gitlab.example.com/api/v4/projects/5/pages/domains"
```
+Create a new Pages domain by using a variable containing the certificate:
+
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "domain=ssl.domain.example" --form "certificate=$CERT_PEM" --form "key=$KEY_PEM" "https://gitlab.example.com/api/v4/projects/5/pages/domains"
```
+Create a new Pages domain with an [automatic certificate](../user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md#enabling-lets-encrypt-integration-for-your-custom-domain):
+
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "domain=ssl.domain.example" --form "auto_ssl_enabled=true" "https://gitlab.example.com/api/v4/projects/5/pages/domains"
```
@@ -157,9 +163,9 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "domain
}
```
-## Update pages domain
+## Update Pages domain
-Updates an existing project pages domain. The user must have permissions to change an existing pages domains.
+Updates an existing project Pages domain. The user must have permissions to change an existing Pages domains.
```plaintext
PUT /projects/:id/pages/domains/:domain
@@ -175,10 +181,14 @@ PUT /projects/:id/pages/domains/:domain
### Adding certificate
+Add a certificate for a Pages domain from a `.pem` file:
+
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form "certificate=@/path/to/cert.pem" --form "key=@/path/to/key.pem" "https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example"
```
+Add a certificate for a Pages domain by using a variable containing the certificate:
+
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form "certificate=$CERT_PEM" --form "key=$KEY_PEM" "https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example"
```
@@ -227,9 +237,9 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form "certifi
}
```
-## Delete pages domain
+## Delete Pages domain
-Deletes an existing project pages domain.
+Deletes an existing project Pages domain.
```plaintext
DELETE /projects/:id/pages/domains/:domain
diff --git a/doc/ci/environments/incremental_rollouts.md b/doc/ci/environments/incremental_rollouts.md
index 5da5c8e0a87..06661e03001 100644
--- a/doc/ci/environments/incremental_rollouts.md
+++ b/doc/ci/environments/incremental_rollouts.md
@@ -19,7 +19,7 @@ tranches after a default pause of 5 minutes.
Timed rollouts can also be manually triggered before the pause period has expired.
Manual and Timed rollouts are included automatically in projects controlled by
-[AutoDevOps](../../topics/autodevops/index.md), but they are also configurable through
+[Auto DevOps](../../topics/autodevops/index.md), but they are also configurable through
GitLab CI/CD in the `.gitlab-ci.yml` configuration file.
Manually triggered rollouts can be implemented with your [Continuously Delivery](../introduction/index.md#continuous-delivery)
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 37ccef10567..34e3b276d3e 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -64,6 +64,7 @@ choose one of these templates:
- [Clojure (`Clojure.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Clojure.gitlab-ci.yml)
- [Composer `Composer.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Composer.gitlab-ci.yml)
- [Crystal (`Crystal.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml)
+- [Dart (`Dart.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Dart.gitlab-ci.yml)
- [Django (`Django.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Django.gitlab-ci.yml)
- [Docker (`Docker.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Docker.gitlab-ci.yml)
- [dotNET (`dotNET.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/dotNET.gitlab-ci.yml)
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index bedf4cfa530..d18f081dbad 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -982,7 +982,7 @@ If you do want to include the `rake test`, see [`before_script` and `after_scrip
possible to inherit from regular jobs as well.
`extends` supports multi-level inheritance. You should avoid using more than 3 levels,
-but you can use as many as ten.
+but you can use as many as eleven.
The following example has two levels of inheritance:
```yaml
diff --git a/doc/install/installation.md b/doc/install/installation.md
index c367d6d2ad2..f0a5970ef87 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -687,7 +687,7 @@ Next, make sure that Gitaly is configured:
sudo chmod 0700 /home/git/gitlab/tmp/sockets/private
sudo chown git /home/git/gitlab/tmp/sockets/private
-# If you are using non-default settings you need to update config.toml
+# If you are using non-default settings, you need to update config.toml
cd /home/git/gitaly
sudo -u git -H editor config.toml
```
@@ -741,7 +741,7 @@ Download the init script (is `/etc/init.d/gitlab`):
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
-And if you are installing with a non-default folder or user copy and edit the defaults file:
+And if you are installing with a non-default folder or user, copy and edit the defaults file:
```shell
sudo cp lib/support/init.d/gitlab.default.example /etc/default/gitlab
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index ab338439d08..7cfccc798d0 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -61,8 +61,8 @@ From GitLab 13.1:
### Node.js versions
-Beginning in GitLab 12.9, we only support node.js 10.13.0 or higher, and we have dropped
-support for node.js 8. (node.js 6 support was dropped in GitLab 11.8)
+Beginning in GitLab 12.9, we only support Node.js 10.13.0 or higher, and we have dropped
+support for Node.js 8. (Node.js 6 support was dropped in GitLab 11.8)
We recommend Node 12.x, as it's faster.
diff --git a/doc/user/project/pages/getting_started/pages_from_scratch.md b/doc/user/project/pages/getting_started/pages_from_scratch.md
index cabaf734d77..664ffa62fd8 100644
--- a/doc/user/project/pages/getting_started/pages_from_scratch.md
+++ b/doc/user/project/pages/getting_started/pages_from_scratch.md
@@ -190,7 +190,7 @@ pages:
- public
```
-Then configure the pipeline to run the job for the master branch only.
+Then configure the pipeline to run the job for the `master` branch only.
```yaml
image: ruby:2.7
diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake
index 31030d061f2..1e28d15f75e 100644
--- a/lib/tasks/gettext.rake
+++ b/lib/tasks/gettext.rake
@@ -38,7 +38,9 @@ namespace :gettext do
Rake::Task['gettext:find'].invoke
# leave only the required changes.
- `git checkout -- locale/*/gitlab.po`
+ unless system(*%w(git checkout -- locale/*/gitlab.po))
+ raise 'failed to cleanup generated locale/*/gitlab.po files'
+ end
# Remove timestamps from the pot file
pot_content = File.read pot_file
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 12ca5ace02f..da902413079 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -13492,6 +13492,15 @@ msgstr ""
msgid "InviteEmail|to join the %{strong_start}%{project_or_group_name}%{strong_end}"
msgstr ""
+msgid "InviteMembersBanner|Collaborate with your team"
+msgstr ""
+
+msgid "InviteMembersBanner|Invite your colleagues"
+msgstr ""
+
+msgid "InviteMembersBanner|We noticed that you haven't invited anyone to this group. Invite your colleagues so you can discuss issues, collaborate on merge requests, and share your knowledge."
+msgstr ""
+
msgid "Invited"
msgstr ""
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index e4e0e21ca15..4886323c836 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -64,7 +64,7 @@ then
echo "Merge request pipeline (detached) detected. Testing all files."
else
MERGE_BASE=$(git merge-base ${CI_MERGE_REQUEST_TARGET_BRANCH_SHA} ${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA})
- MD_DOC_PATH=$(git diff --name-only "${MERGE_BASE}..${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}" '*.md')
+ MD_DOC_PATH=$(git diff --name-only "${MERGE_BASE}..${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}" 'doc/*.md')
echo -e "Merged results pipeline detected. Testing only the following files:\n${MD_DOC_PATH}"
fi
diff --git a/spec/frontend/groups/components/invite_members_banner_spec.js b/spec/frontend/groups/components/invite_members_banner_spec.js
new file mode 100644
index 00000000000..f86091d427c
--- /dev/null
+++ b/spec/frontend/groups/components/invite_members_banner_spec.js
@@ -0,0 +1,76 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlBanner } from '@gitlab/ui';
+import InviteMembersBanner from '~/groups/components/invite_members_banner.vue';
+
+const expectedTitle = 'Collaborate with your team';
+const expectedBody =
+ "We noticed that you haven't invited anyone to this group. Invite your colleagues so you can discuss issues, collaborate on merge requests, and share your knowledge";
+const expectedSvgPath = '/illustrations/background';
+const expectedInviteMembersPath = 'groups/members';
+const expectedButtonText = 'Invite your colleagues';
+
+const createComponent = (stubs = {}) => {
+ return shallowMount(InviteMembersBanner, {
+ provide: {
+ svgPath: expectedSvgPath,
+ inviteMembersPath: expectedInviteMembersPath,
+ },
+ stubs,
+ });
+};
+
+describe('InviteMembersBanner', () => {
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('rendering', () => {
+ const findBanner = () => {
+ return wrapper.find(GlBanner);
+ };
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('uses the svgPath for the banner svgpath', () => {
+ expect(findBanner().attributes('svgpath')).toBe(expectedSvgPath);
+ });
+
+ it('uses the title from options for title', () => {
+ expect(findBanner().attributes('title')).toBe(expectedTitle);
+ });
+
+ it('includes the body text from options', () => {
+ expect(findBanner().html()).toContain(expectedBody);
+ });
+
+ it('uses the button_text text from options for buttontext', () => {
+ expect(findBanner().attributes('buttontext')).toBe(expectedButtonText);
+ });
+
+ it('uses the href from inviteMembersPath for buttonlink', () => {
+ expect(findBanner().attributes('buttonlink')).toBe(expectedInviteMembersPath);
+ });
+ });
+
+ describe('dismissing', () => {
+ const findButton = () => {
+ return wrapper.find('button');
+ };
+ const stubs = {
+ GlBanner,
+ };
+
+ it('sets visible to false', () => {
+ wrapper = createComponent(stubs);
+
+ findButton().trigger('click');
+
+ expect(wrapper.vm.visible).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
index 8446f0f50c4..be75a5bfbdc 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
@@ -20,7 +20,6 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
</label>
<gl-form-group-stub
- class="gl-mb-0"
id="visibility-level-setting"
>
<gl-form-radio-group-stub
@@ -91,12 +90,5 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
</gl-form-radio-stub>
</gl-form-radio-group-stub>
</gl-form-group-stub>
-
- <div
- class="text-muted"
- data-testid="restricted-levels-info"
- >
- <!---->
- </div>
</div>
`;
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index b6abb9f389a..33152591d42 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -102,13 +102,6 @@ describe('Snippet Edit app', () => {
markdownDocsPath: 'http://docs.foo.bar',
...props,
},
- data() {
- return {
- snippet: {
- visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
- },
- };
- },
});
}
diff --git a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
index 3919e4d7993..4ba3e906fc3 100644
--- a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
@@ -1,55 +1,31 @@
import { GlFormRadio, GlIcon, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
-import { defaultSnippetVisibilityLevels } from '~/snippets/utils/blob';
import {
SNIPPET_VISIBILITY,
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
- SNIPPET_LEVELS_RESTRICTED,
- SNIPPET_LEVELS_DISABLED,
} from '~/snippets/constants';
describe('Snippet Visibility Edit component', () => {
let wrapper;
const defaultHelpLink = '/foo/bar';
const defaultVisibilityLevel = 'private';
- const defaultVisibility = defaultSnippetVisibilityLevels([0, 10, 20]);
-
- function createComponent({
- propsData = {},
- visibilityLevels = defaultVisibility,
- multipleLevelsRestricted = false,
- deep = false,
- } = {}) {
- const method = deep ? mount : shallowMount;
- const $apollo = {
- queries: {
- defaultVisibility: {
- loading: false,
- },
- },
- };
+ function createComponent(propsData = {}, deep = false) {
+ const method = deep ? mount : shallowMount;
wrapper = method.call(this, SnippetVisibilityEdit, {
- mock: { $apollo },
propsData: {
helpLink: defaultHelpLink,
isProjectSnippet: false,
value: defaultVisibilityLevel,
...propsData,
},
- data() {
- return {
- visibilityLevels,
- multipleLevelsRestricted,
- };
- },
});
}
- const findLink = () => wrapper.find('label').find(GlLink);
+ const findLabel = () => wrapper.find('label');
const findRadios = () => wrapper.find(GlFormRadioGroup).findAll(GlFormRadio);
const findRadiosData = () =>
findRadios().wrappers.map(x => {
@@ -71,84 +47,60 @@ describe('Snippet Visibility Edit component', () => {
expect(wrapper.element).toMatchSnapshot();
});
- it('renders label help link', () => {
- createComponent();
-
- expect(findLink().attributes('href')).toBe(defaultHelpLink);
- });
-
- it('when helpLink is not defined, does not render label help link', () => {
- createComponent({ propsData: { helpLink: null } });
+ it('renders visibility options', () => {
+ createComponent({}, true);
- expect(findLink().exists()).toBe(false);
- });
-
- describe('Visibility options', () => {
- const findRestrictedInfo = () => wrapper.find('[data-testid="restricted-levels-info"]');
- const RESULTING_OPTIONS = {
- 0: {
+ expect(findRadiosData()).toEqual([
+ {
value: SNIPPET_VISIBILITY_PRIVATE,
icon: SNIPPET_VISIBILITY.private.icon,
text: SNIPPET_VISIBILITY.private.label,
description: SNIPPET_VISIBILITY.private.description,
},
- 10: {
+ {
value: SNIPPET_VISIBILITY_INTERNAL,
icon: SNIPPET_VISIBILITY.internal.icon,
text: SNIPPET_VISIBILITY.internal.label,
description: SNIPPET_VISIBILITY.internal.description,
},
- 20: {
+ {
value: SNIPPET_VISIBILITY_PUBLIC,
icon: SNIPPET_VISIBILITY.public.icon,
text: SNIPPET_VISIBILITY.public.label,
description: SNIPPET_VISIBILITY.public.description,
},
- };
+ ]);
+ });
+
+ it('when project snippet, renders special private description', () => {
+ createComponent({ isProjectSnippet: true }, true);
- it.each`
- levels | resultOptions
- ${undefined} | ${[]}
- ${''} | ${[]}
- ${[]} | ${[]}
- ${[0]} | ${[RESULTING_OPTIONS[0]]}
- ${[0, 10]} | ${[RESULTING_OPTIONS[0], RESULTING_OPTIONS[10]]}
- ${[0, 10, 20]} | ${[RESULTING_OPTIONS[0], RESULTING_OPTIONS[10], RESULTING_OPTIONS[20]]}
- ${[0, 20]} | ${[RESULTING_OPTIONS[0], RESULTING_OPTIONS[20]]}
- ${[10, 20]} | ${[RESULTING_OPTIONS[10], RESULTING_OPTIONS[20]]}
- `('renders correct visibility options for $levels', ({ levels, resultOptions }) => {
- createComponent({ visibilityLevels: defaultSnippetVisibilityLevels(levels), deep: true });
- expect(findRadiosData()).toEqual(resultOptions);
+ expect(findRadiosData()[0]).toEqual({
+ value: SNIPPET_VISIBILITY_PRIVATE,
+ icon: SNIPPET_VISIBILITY.private.icon,
+ text: SNIPPET_VISIBILITY.private.label,
+ description: SNIPPET_VISIBILITY.private.description_project,
});
+ });
- it.each`
- levels | levelsRestricted | resultText
- ${[]} | ${false} | ${SNIPPET_LEVELS_DISABLED}
- ${[]} | ${true} | ${SNIPPET_LEVELS_DISABLED}
- ${[0]} | ${true} | ${SNIPPET_LEVELS_RESTRICTED}
- ${[0]} | ${false} | ${''}
- ${[0, 10, 20]} | ${false} | ${''}
- `(
- 'renders correct information about restricted visibility levels for $levels',
- ({ levels, levelsRestricted, resultText }) => {
- createComponent({
- visibilityLevels: defaultSnippetVisibilityLevels(levels),
- multipleLevelsRestricted: levelsRestricted,
- });
- expect(findRestrictedInfo().text()).toBe(resultText);
- },
- );
+ it('renders label help link', () => {
+ createComponent();
- it('when project snippet, renders special private description', () => {
- createComponent({ propsData: { isProjectSnippet: true }, deep: true });
+ expect(
+ findLabel()
+ .find(GlLink)
+ .attributes('href'),
+ ).toBe(defaultHelpLink);
+ });
- expect(findRadiosData()[0]).toEqual({
- value: SNIPPET_VISIBILITY_PRIVATE,
- icon: SNIPPET_VISIBILITY.private.icon,
- text: SNIPPET_VISIBILITY.private.label,
- description: SNIPPET_VISIBILITY.private.description_project,
- });
- });
+ it('when helpLink is not defined, does not render label help link', () => {
+ createComponent({ helpLink: null });
+
+ expect(
+ findLabel()
+ .find(GlLink)
+ .exists(),
+ ).toBe(false);
});
});
@@ -156,7 +108,7 @@ describe('Snippet Visibility Edit component', () => {
it('pre-selects correct option in the list', () => {
const value = SNIPPET_VISIBILITY_INTERNAL;
- createComponent({ propsData: { value } });
+ createComponent({ value });
expect(wrapper.find(GlFormRadioGroup).attributes('checked')).toBe(value);
});
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 0790dc1b674..08b25d64b43 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -369,4 +369,48 @@ RSpec.describe GroupsHelper do
it { is_expected.to be_falsey }
end
end
+
+ describe '#show_invite_banner?' do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be_with_refind(:group) { create(:group) }
+ let_it_be(:users) { [current_user, create(:user)] }
+
+ subject { helper.show_invite_banner?(group) }
+
+ before do
+ allow(helper).to receive(:current_user) { current_user }
+ allow(helper).to receive(:can?).with(current_user, :admin_group, group).and_return(can_admin_group)
+ stub_feature_flags(invite_your_teammates_banner_a: feature_enabled_flag)
+ users.take(group_members_count).each { |user| group.add_guest(user) }
+ end
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:feature_enabled_flag, :can_admin_group, :group_members_count, :expected_result) do
+ true | true | 1 | true
+ true | false | 1 | false
+ false | true | 1 | false
+ false | false | 1 | false
+ true | true | 2 | false
+ true | false | 2 | false
+ false | true | 2 | false
+ false | false | 2 | false
+ end
+
+ with_them do
+ context 'when the group was just created' do
+ before do
+ flash[:notice] = "Group #{group.name} was successfully created"
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when no flash message' do
+ it 'returns the expected result' do
+ expect(subject).to eq(expected_result)
+ end
+ end
+ end
+ end
end