Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
dc965b8cc8
commit
583bde3f83
32 changed files with 333 additions and 221 deletions
|
@ -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>
|
21
app/assets/javascripts/groups/init_invite_members_banner.js
Normal file
21
app/assets/javascripts/groups/init_invite_members_banner.js
Normal file
|
@ -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),
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.',
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
query defaultSnippetVisibility {
|
||||
visibilityLevels @client
|
||||
selectedLevel @client
|
||||
multipleLevelsRestricted @client
|
||||
}
|
|
@ -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 [];
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
5
changelogs/unreleased/cat-time-precision-2fa-ldap.yml
Normal file
5
changelogs/unreleased/cat-time-precision-2fa-ldap.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update the 2FA user update check to account for rounding errors
|
||||
merge_request: 41327
|
||||
author:
|
||||
type: fixed
|
|
@ -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
|
|
@ -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.*\)'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -102,13 +102,6 @@ describe('Snippet Edit app', () => {
|
|||
markdownDocsPath: 'http://docs.foo.bar',
|
||||
...props,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
snippet: {
|
||||
visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
} = {}) {
|
||||
function createComponent(propsData = {}, deep = false) {
|
||||
const method = deep ? mount : shallowMount;
|
||||
const $apollo = {
|
||||
queries: {
|
||||
defaultVisibility: {
|
||||
loading: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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,76 +47,33 @@ describe('Snippet Visibility Edit component', () => {
|
|||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders label help link', () => {
|
||||
createComponent();
|
||||
it('renders visibility options', () => {
|
||||
createComponent({}, true);
|
||||
|
||||
expect(findLink().attributes('href')).toBe(defaultHelpLink);
|
||||
});
|
||||
|
||||
it('when helpLink is not defined, does not render label help link', () => {
|
||||
createComponent({ propsData: { helpLink: null } });
|
||||
|
||||
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.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);
|
||||
]);
|
||||
});
|
||||
|
||||
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('when project snippet, renders special private description', () => {
|
||||
createComponent({ propsData: { isProjectSnippet: true }, deep: true });
|
||||
createComponent({ isProjectSnippet: true }, true);
|
||||
|
||||
expect(findRadiosData()[0]).toEqual({
|
||||
value: SNIPPET_VISIBILITY_PRIVATE,
|
||||
|
@ -149,6 +82,25 @@ describe('Snippet Visibility Edit component', () => {
|
|||
description: SNIPPET_VISIBILITY.private.description_project,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders label help link', () => {
|
||||
createComponent();
|
||||
|
||||
expect(
|
||||
findLabel()
|
||||
.find(GlLink)
|
||||
.attributes('href'),
|
||||
).toBe(defaultHelpLink);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue