diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-04 03:08:22 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-04 03:08:22 +0000 |
commit | 583bde3f83951fa4c294804edc2e9c57fb293733 (patch) | |
tree | 83900919e93ea9c1dab7571c9d4e02e73d8b8fb5 | |
parent | dc965b8cc88f8dadf879c0d80214864c699ebf1f (diff) | |
download | gitlab-ce-583bde3f83951fa4c294804edc2e9c57fb293733.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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 |