Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e7fb614993
commit
32bbedbc21
158 changed files with 2467 additions and 1445 deletions
|
@ -93,6 +93,10 @@ rules:
|
|||
group: internal
|
||||
alphabetize:
|
||||
order: ignore
|
||||
'no-restricted-syntax':
|
||||
- error
|
||||
- selector: ImportSpecifier[imported.name='GlSkeletonLoading']
|
||||
message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.'
|
||||
overrides:
|
||||
- files:
|
||||
- '{,ee/,jh/}spec/frontend*/**/*'
|
||||
|
@ -107,6 +111,8 @@ overrides:
|
|||
message: 'Using $nextTick from a component instance is discouraged. Import nextTick directly from the Vue package.'
|
||||
- selector: Identifier[name='setImmediate']
|
||||
message: 'Prefer explicit waitForPromises (or equivalent), or jest.runAllTimers (or equivalent) to vague setImmediate calls.'
|
||||
- selector: ImportSpecifier[imported.name='GlSkeletonLoading']
|
||||
message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.'
|
||||
- files:
|
||||
- 'config/**/*'
|
||||
- 'scripts/**/*'
|
||||
|
|
|
@ -8,17 +8,17 @@
|
|||
|
||||
## Author's checklist
|
||||
|
||||
- [ ] Consider taking [the GitLab Technical Writing Fundamentals course](https://gitlab.edcast.com/pathways/ECL-02528ee2-c334-4e16-abf3-e9d8b8260de4).
|
||||
- [ ] Optional. Consider taking [the GitLab Technical Writing Fundamentals course](https://gitlab.edcast.com/pathways/ECL-02528ee2-c334-4e16-abf3-e9d8b8260de4).
|
||||
- [ ] Follow the:
|
||||
- [Documentation process](https://docs.gitlab.com/ee/development/documentation/workflow.html).
|
||||
- [Documentation guidelines](https://docs.gitlab.com/ee/development/documentation/).
|
||||
- [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide/).
|
||||
- [ ] Ensure that the [product tier badge](https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#product-tier-badges) is added to topic's `h1`.
|
||||
- [ ] [Request a review](https://docs.gitlab.com/ee/development/code_review.html#dogfooding-the-reviewers-feature) based on:
|
||||
- [ ] If you're adding or changing the main heading of the page (H1), ensure that the [product tier badge](https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#product-tier-badges) is added.
|
||||
- [ ] If you are a GitLab team member, [request a review](https://docs.gitlab.com/ee/development/code_review.html#dogfooding-the-attention-request-feature) based on:
|
||||
- The documentation page's [metadata](https://docs.gitlab.com/ee/development/documentation/#metadata).
|
||||
- The [associated Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments).
|
||||
|
||||
If you are only adding documentation, do not add any of the following labels:
|
||||
If you are a GitLab team member and only adding documentation, do not add any of the following labels:
|
||||
|
||||
- `~"frontend"`
|
||||
- `~"backend"`
|
||||
|
@ -27,7 +27,7 @@ If you are only adding documentation, do not add any of the following labels:
|
|||
|
||||
These labels cause the MR to be added to code verification QA issues.
|
||||
|
||||
## Review checklist
|
||||
## Reviewer's checklist
|
||||
|
||||
Documentation-related MRs should be reviewed by a Technical Writer for a non-blocking review, based on [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/) and the [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide/).
|
||||
|
||||
|
@ -35,13 +35,13 @@ Documentation-related MRs should be reviewed by a Technical Writer for a non-blo
|
|||
- Technical writer review items:
|
||||
- [ ] Ensure docs metadata is present and up-to-date.
|
||||
- [ ] Ensure the appropriate [labels](https://about.gitlab.com/handbook/engineering/ux/technical-writing/workflow/#labels) are added to this MR.
|
||||
- [ ] Ensure a release milestone is set.
|
||||
- If relevant to this MR, ensure [content topic type](https://docs.gitlab.com/ee/development/documentation/structure.html) principles are in use, including:
|
||||
- [ ] The headings should be something you'd do a Google search for. Instead of `Default behavior`, say something like `Default behavior when you close an issue`.
|
||||
- [ ] The headings (other than the page title) should be active. Instead of `Configuring GDK`, say something like `Configure GDK`.
|
||||
- [ ] Any task steps should be written as a numbered list.
|
||||
- If the content still needs to be edited for topic types, you can create a follow-up issue with the ~"docs-technical-debt" label.
|
||||
- [ ] Review by assigned maintainer, who can always request/require the reviews above. Maintainer's review can occur before or after a technical writer review.
|
||||
- [ ] Ensure a release milestone is set.
|
||||
|
||||
/label ~documentation ~"type::maintenance"
|
||||
/label ~documentation ~"type::maintenance" ~"docs::improvement"
|
||||
/assign me
|
||||
|
|
77
CHANGELOG.md
77
CHANGELOG.md
|
@ -2,6 +2,31 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 14.9.2 (2022-03-31)
|
||||
|
||||
### Security (20 changes)
|
||||
|
||||
- [Quarantine UsageDataNonSqlMetrics failing test](gitlab-org/security/gitlab@123fc00ff9f407284ce05007ddc373e1bd0aeede) ([merge request](gitlab-org/security/gitlab!2364))
|
||||
- [Disallow login if password matches a fixed list](gitlab-org/security/gitlab@1a128ae3fb17b3d83974bb08034e4ba7a7d54e3b) ([merge request](gitlab-org/security/gitlab!2357))
|
||||
- [Update devise-two-factor to 4.0.2](gitlab-org/security/gitlab@17c70b13dcd437c05de63b3286245af8e6f42210) ([merge request](gitlab-org/security/gitlab!2349))
|
||||
- [Limit the number of tags associated with a CI runner](gitlab-org/security/gitlab@ed5daced882a0206e050c4f676a888ac1c2417b1) ([merge request](gitlab-org/security/gitlab!2303))
|
||||
- [GitLab Pages Security Updates for 14.9](gitlab-org/security/gitlab@79709cabf71a57a336f490636a7e32a208fe0229) ([merge request](gitlab-org/security/gitlab!2327))
|
||||
- [Upgrade swagger-ui dependency](gitlab-org/security/gitlab@14280c1d844be3ffc2f30f5321a818a7b6c51770) ([merge request](gitlab-org/security/gitlab!2336))
|
||||
- [Modify release link format check to avoid regex if string is too long](gitlab-org/security/gitlab@f516d883b46e1441410476dc140d69fde51cdf0f) ([merge request](gitlab-org/security/gitlab!2307))
|
||||
- [Masks variables in error messages](gitlab-org/security/gitlab@9cf62118390c0cfba3d36a4231a30a7836f06e2f) ([merge request](gitlab-org/security/gitlab!2308))
|
||||
- [Escape user provided string to prevent XSS](gitlab-org/security/gitlab@2da3502aef64ed1b01c13d82418950cf284098c6) ([merge request](gitlab-org/security/gitlab!2313))
|
||||
- [Monkey patch of RDoc to prevent Ruby segfault](gitlab-org/security/gitlab@0ae4925089a1b5fd7c9abeeb0756b3a50e05799a) ([merge request](gitlab-org/security/gitlab!2321))
|
||||
- [Project import maps members' created_by_id users based on source user ID](gitlab-org/security/gitlab@3826f2a7c652d3f74e45bfef8888601ca1c86ba1) ([merge request](gitlab-org/security/gitlab!2301))
|
||||
- [Redact InvalidURIError error messages](gitlab-org/security/gitlab@59b60e9cf8f79d6f41000d34a4434c5a04988030) ([merge request](gitlab-org/security/gitlab!2295))
|
||||
- [Fix access for approval rules API](gitlab-org/security/gitlab@7890215aa29624cd67c5bc8ac25175f2866479b7) ([merge request](gitlab-org/security/gitlab!2322))
|
||||
- [Fix kroki exploit](gitlab-org/security/gitlab@b2a44b407ab85ca056a271ba4e708128ef08d25f) ([merge request](gitlab-org/security/gitlab!2306))
|
||||
- [Fix blind SSRF when looking up SSH host keys for mirroring](gitlab-org/security/gitlab@5a9509b52584302c508bd6dff1454f80aae371ea) ([merge request](gitlab-org/security/gitlab!2309))
|
||||
- [Escape original content in reference redactor](gitlab-org/security/gitlab@b33b170a2c2df8285999f3631e8a53d35e0eed22) ([merge request](gitlab-org/security/gitlab!2317))
|
||||
- [Security fix for CI/CD analytics visibility](gitlab-org/security/gitlab@f3febd00b440475b2aca0b9bd6728fa5f8750288) ([merge request](gitlab-org/security/gitlab!2304))
|
||||
- [Latest commit exposed through fork of a private project](gitlab-org/security/gitlab@3f20d4f294a12ceb33bec19d86790f582fb7fb48) ([merge request](gitlab-org/security/gitlab!2294))
|
||||
- [Fix Asana integration restricted branch filter](gitlab-org/security/gitlab@08aa0f55b1b715f7311ee6502cd6f8a1b875f878) ([merge request](gitlab-org/security/gitlab!2300))
|
||||
- [Revert "JH need more complex passwords"](gitlab-org/security/gitlab@e2fb87ec5d4e235d6b83454980cec9c049849a1c) ([merge request](gitlab-org/security/gitlab!2352))
|
||||
|
||||
## 14.9.1 (2022-03-23)
|
||||
|
||||
### Fixed (1 change)
|
||||
|
@ -604,6 +629,32 @@ entry.
|
|||
- [Clean up issue_boards_filtered_search feature flag](gitlab-org/gitlab@a97ed09ffb0d88007b21a314ab48b2e50d7c4bfa) ([merge request](gitlab-org/gitlab!80771))
|
||||
- [Add table for storing issue tsvector](gitlab-org/gitlab@ceabf5a8ad0d67768b05a58a84b242495645a57c) ([merge request](gitlab-org/gitlab!71913))
|
||||
|
||||
## 14.8.5 (2022-03-31)
|
||||
|
||||
### Security (21 changes)
|
||||
|
||||
- [Update to commonmarker 0.23.4](gitlab-org/security/gitlab@51532ccc5f1b6b053d4ca6c54496607e62f8f25c) ([merge request](gitlab-org/security/gitlab!2282))
|
||||
- [Revert merge request approval groups behavior](gitlab-org/security/gitlab@dd9724e429033974da6c3852dc6fd33f0f2b0a46) ([merge request](gitlab-org/security/gitlab!2334))
|
||||
- [Disallow login if password matches a fixed list](gitlab-org/security/gitlab@6779d5f2948425a7ad7f19a6e10f82cc10b80989) ([merge request](gitlab-org/security/gitlab!2358))
|
||||
- [Update devise-two-factor to 4.0.2](gitlab-org/security/gitlab@0329d2d82a9064c0bae36e7b993ee40df7c999bc) ([merge request](gitlab-org/security/gitlab!2350))
|
||||
- [Limit the number of tags associated with a CI runner](gitlab-org/security/gitlab@8d5938c08fe66c22f1bc54ff76cc9daf2de86b1a) ([merge request](gitlab-org/security/gitlab!2302))
|
||||
- [GitLab Pages Security Updates for 14.9](gitlab-org/security/gitlab@5a5a862c8a9e37ca2ea84133f92b216eaa7cd148) ([merge request](gitlab-org/security/gitlab!2328))
|
||||
- [Upgrade swagger-ui dependency](gitlab-org/security/gitlab@afcb570867db61347bb6a4e243bb2557340191be) ([merge request](gitlab-org/security/gitlab!2337))
|
||||
- [Modify release link format check to avoid regex if string is too long](gitlab-org/security/gitlab@a3ab0ff9c470c1c6e5b4fd055ddd02dffce32652) ([merge request](gitlab-org/security/gitlab!2243))
|
||||
- [Masks variables in error messages](gitlab-org/security/gitlab@94236bbdb8eef6600562bdc4e242e07eaed8c50f) ([merge request](gitlab-org/security/gitlab!2291))
|
||||
- [Escape user provided string to prevent XSS](gitlab-org/security/gitlab@03e695d4c34546582b503b3f7712246206b56b99) ([merge request](gitlab-org/security/gitlab!2314))
|
||||
- [Monkey patch of RDoc to prevent Ruby segfault](gitlab-org/security/gitlab@14eec4487387bc0c999f1c48b046a3ed3848c5a1) ([merge request](gitlab-org/security/gitlab!2232))
|
||||
- [Project import maps members' created_by_id users based on source user ID](gitlab-org/security/gitlab@7fd7ab3f57e8d8b4e0aed42aebe9a8b7436a6255) ([merge request](gitlab-org/security/gitlab!2238))
|
||||
- [Redact InvalidURIError error messages](gitlab-org/security/gitlab@0592c182bfd60aee501c4c66f47a71c9469f2bcd) ([merge request](gitlab-org/security/gitlab!2296))
|
||||
- [Fix access for approval rules API](gitlab-org/security/gitlab@987e06bacba224519adf94cda73b5a8b2e7b917a) ([merge request](gitlab-org/security/gitlab!2323))
|
||||
- [Fix kroki exploit](gitlab-org/security/gitlab@bf056c683af25ec4b94c0efa7166eea399ed6502) ([merge request](gitlab-org/security/gitlab!2277))
|
||||
- [Fix blind SSRF when looking up SSH host keys for mirroring](gitlab-org/security/gitlab@3c853a32a73aba15e309d05111b744455a360cca) ([merge request](gitlab-org/security/gitlab!2310))
|
||||
- [Escape original content in reference redactor](gitlab-org/security/gitlab@00ee99bc3834d9d59572272064c9ad6abeae5975) ([merge request](gitlab-org/security/gitlab!2318))
|
||||
- [Security fix for CI/CD analytics visibility](gitlab-org/security/gitlab@691d69be77ae3c8e0a2598b75ccf336b672fd540) ([merge request](gitlab-org/security/gitlab!2273))
|
||||
- [Latest commit exposed through fork of a private project](gitlab-org/security/gitlab@6ca7a3b040edac06b23a697bfc2bf46f457d6b81) ([merge request](gitlab-org/security/gitlab!2271))
|
||||
- [Fix Asana integration restricted branch filter](gitlab-org/security/gitlab@4c1db692b4e99fab6cdbb818cf02fb879f6d4886) ([merge request](gitlab-org/security/gitlab!2218))
|
||||
- [Revert "JH need more complex passwords"](gitlab-org/security/gitlab@919aa2b28645d49fb71508362a0c61da39893c69) ([merge request](gitlab-org/security/gitlab!2353))
|
||||
|
||||
## 14.8.4 (2022-03-16)
|
||||
|
||||
### Added (1 change)
|
||||
|
@ -1319,6 +1370,32 @@ entry.
|
|||
- [Use `ssh_data` gem instead of `net-ssh` and `sshkey` where possible](gitlab-org/gitlab@59a0ee8605d509753c9aec719f8e0da77bcc679d) ([merge request](gitlab-org/gitlab!77424))
|
||||
- [Remove feature flag already default enabled](gitlab-org/gitlab@9b7059a4bf9dc2ecdce1910a931cc6967d05b5ad) ([merge request](gitlab-org/gitlab!78238)) **GitLab Enterprise Edition**
|
||||
|
||||
## 14.7.7 (2022-03-31)
|
||||
|
||||
### Security (21 changes)
|
||||
|
||||
- [Update to commonmarker 0.23.4](gitlab-org/security/gitlab@eb4b231173c86901f93b5b7781716b1f7706dad1) ([merge request](gitlab-org/security/gitlab!2283))
|
||||
- [Revert merge request approval groups behavior](gitlab-org/security/gitlab@08e3ecced649f6ad241db6de7050b1502f7bef21) ([merge request](gitlab-org/security/gitlab!2333))
|
||||
- [Disallow login if password matches a fixed list](gitlab-org/security/gitlab@02a69ab32da1ac67d855de3ee388d0bd2bb6586e) ([merge request](gitlab-org/security/gitlab!2359))
|
||||
- [Update devise-two-factor to 4.0.2](gitlab-org/security/gitlab@c9fde96c7780f5b883cd1ac63d7ac3d5f4d78dc6) ([merge request](gitlab-org/security/gitlab!2351))
|
||||
- [Limit the number of tags associated with a CI runner](gitlab-org/security/gitlab@00124d5f8ba0d7437d1f6f19b029754bf481185b) ([merge request](gitlab-org/security/gitlab!2305))
|
||||
- [GitLab Pages Security Updates for 14.9](gitlab-org/security/gitlab@d335917e233658fa9d4452053469c3582ef38368) ([merge request](gitlab-org/security/gitlab!2325))
|
||||
- [Upgrade swagger-ui dependency](gitlab-org/security/gitlab@7a8ce32f70fd0338817705651ee0dbe0a277d5f1) ([merge request](gitlab-org/security/gitlab!2338))
|
||||
- [Modify release link format check to avoid regex if string is too long](gitlab-org/security/gitlab@e18dc2be245bca7e192c8536d1ba7de2ad798c43) ([merge request](gitlab-org/security/gitlab!2244))
|
||||
- [Masks variables in error messages](gitlab-org/security/gitlab@1706c5cf9b939a6ab0682db7b8945feb851a3f8b) ([merge request](gitlab-org/security/gitlab!2292))
|
||||
- [Escape user provided string to prevent XSS](gitlab-org/security/gitlab@c57edf9ab52810d455e41d71bad4e4d12c098cad) ([merge request](gitlab-org/security/gitlab!2315))
|
||||
- [Monkey patch of RDoc to prevent Ruby segfault](gitlab-org/security/gitlab@f9e5597d1864d03bf1f0103787becbc84886968d) ([merge request](gitlab-org/security/gitlab!2233))
|
||||
- [Project import maps members' created_by_id users based on source user ID](gitlab-org/security/gitlab@3ea1e477e0596f15e040f42b59fa86953d057128) ([merge request](gitlab-org/security/gitlab!2239))
|
||||
- [Redact InvalidURIError error messages](gitlab-org/security/gitlab@a42ede835e32f44b68c1affe78a7ee48332bb30a) ([merge request](gitlab-org/security/gitlab!2297))
|
||||
- [Fix access for approval rules API](gitlab-org/security/gitlab@b8c3997763d1e041dc2b82e464a99a5b2f15a798) ([merge request](gitlab-org/security/gitlab!2324))
|
||||
- [Fix kroki exploit](gitlab-org/security/gitlab@ad123e33510103af4fb00378ef1fc8dae4cacb21) ([merge request](gitlab-org/security/gitlab!2278))
|
||||
- [Fix blind SSRF when looking up SSH host keys for mirroring](gitlab-org/security/gitlab@0209f44cb4876f0a9ef13d4c8875a95a0cda1e2f) ([merge request](gitlab-org/security/gitlab!2311))
|
||||
- [Escape original content in reference redactor](gitlab-org/security/gitlab@f63861d8fe7b2b8d161162063e7995782cbfada8) ([merge request](gitlab-org/security/gitlab!2319))
|
||||
- [Security fix for CI/CD analytics visibility](gitlab-org/security/gitlab@fea6a4ff80862f9dba493405d03d82cf129e8854) ([merge request](gitlab-org/security/gitlab!2274))
|
||||
- [Latest commit exposed through fork of a private project](gitlab-org/security/gitlab@b573cea38cdce020e5f25fb9de60e0e506c87a9b) ([merge request](gitlab-org/security/gitlab!2272))
|
||||
- [Fix Asana integration restricted branch filter](gitlab-org/security/gitlab@56e2d9ae3de4f587d2c8a5aa111c2922553d6b7b) ([merge request](gitlab-org/security/gitlab!2214))
|
||||
- [Revert "JH need more complex passwords"](gitlab-org/security/gitlab@2419522b02700ce98e0c4d6e7bfd4d28b6464506) ([merge request](gitlab-org/security/gitlab!2354))
|
||||
|
||||
## 14.7.6 (2022-03-24)
|
||||
|
||||
### Added (1 change)
|
||||
|
|
|
@ -1 +1 @@
|
|||
1.56.0
|
||||
1.56.1
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -67,7 +67,7 @@ gem 'akismet', '~> 3.0'
|
|||
gem 'invisible_captcha', '~> 1.1.0'
|
||||
|
||||
# Two-factor authentication
|
||||
gem 'devise-two-factor', '~> 4.0.0'
|
||||
gem 'devise-two-factor', '~> 4.0.2'
|
||||
gem 'rqrcode-rails3', '~> 0.1.7'
|
||||
gem 'attr_encrypted', '~> 3.1.0'
|
||||
gem 'u2f', '~> 0.2.1'
|
||||
|
|
|
@ -270,11 +270,11 @@ GEM
|
|||
railties (>= 4.1.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise-two-factor (4.0.0)
|
||||
activesupport (< 6.2)
|
||||
devise-two-factor (4.0.2)
|
||||
activesupport (< 7.1)
|
||||
attr_encrypted (>= 1.3, < 4, != 2)
|
||||
devise (~> 4.0)
|
||||
railties (< 6.2)
|
||||
railties (< 7.1)
|
||||
rotp (~> 6.0)
|
||||
diff-lcs (1.4.4)
|
||||
diff_match_patch (0.1.0)
|
||||
|
@ -1458,7 +1458,7 @@ DEPENDENCIES
|
|||
derailed_benchmarks
|
||||
device_detector
|
||||
devise (~> 4.7.2)
|
||||
devise-two-factor (~> 4.0.0)
|
||||
devise-two-factor (~> 4.0.2)
|
||||
diff_match_patch (~> 0.1.0)
|
||||
diffy (~> 3.3)
|
||||
discordrb-webhooks (~> 3.4)
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<script>
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
bodyText: __('Warning: Displaying this diagram might cause performance issues on this page.'),
|
||||
buttonText: __('Display'),
|
||||
},
|
||||
components: {
|
||||
GlAlert,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-alert
|
||||
:primary-button-text="$options.i18n.buttonText"
|
||||
variant="warning"
|
||||
@dismiss="$emit('closeAlert')"
|
||||
@primaryAction="$emit('showImage')"
|
||||
>
|
||||
{{ $options.i18n.bodyText }}
|
||||
</gl-alert>
|
||||
</template>
|
|
@ -1,3 +1,19 @@
|
|||
// https://prosemirror.net/docs/ref/#model.ParseRule.priority
|
||||
export const DEFAULT_PARSE_RULE_PRIORITY = 50;
|
||||
export const HIGHER_PARSE_RULE_PRIORITY = 1 + DEFAULT_PARSE_RULE_PRIORITY;
|
||||
|
||||
export const unrestrictedPages = [
|
||||
// Group wiki
|
||||
'groups:wikis:show',
|
||||
'groups:wikis:edit',
|
||||
'groups:wikis:create',
|
||||
|
||||
// Project wiki
|
||||
'projects:wikis:show',
|
||||
'projects:wikis:edit',
|
||||
'projects:wikis:create',
|
||||
|
||||
// Project files
|
||||
'projects:show',
|
||||
'projects:blob:show',
|
||||
];
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import $ from 'jquery';
|
||||
import syntaxHighlight from '~/syntax_highlight';
|
||||
import highlightCurrentUser from './highlight_current_user';
|
||||
import { renderKroki } from './render_kroki';
|
||||
import renderMath from './render_math';
|
||||
import renderMermaid from './render_mermaid';
|
||||
import renderSandboxedMermaid from './render_sandboxed_mermaid';
|
||||
|
@ -12,6 +13,7 @@ import renderMetrics from './render_metrics';
|
|||
//
|
||||
$.fn.renderGFM = function renderGFM() {
|
||||
syntaxHighlight(this.find('.js-syntax-highlight').get());
|
||||
renderKroki(this.find('.js-render-kroki[hidden]').get());
|
||||
renderMath(this.find('.js-render-math'));
|
||||
if (gon.features?.sandboxedMermaid) {
|
||||
renderSandboxedMermaid(this.find('.js-render-mermaid'));
|
||||
|
|
63
app/assets/javascripts/behaviors/markdown/render_kroki.js
Normal file
63
app/assets/javascripts/behaviors/markdown/render_kroki.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
import Vue from 'vue';
|
||||
import DiagramPerformanceWarning from '../components/diagram_performance_warning.vue';
|
||||
import { unrestrictedPages } from './constants';
|
||||
|
||||
/**
|
||||
* Create alert element.
|
||||
*
|
||||
* @param {Element} krokiImage Kroki `img` element
|
||||
* @return {Element} Alert element
|
||||
*/
|
||||
function createAlert(krokiImage) {
|
||||
const app = new Vue({
|
||||
el: document.createElement('div'),
|
||||
name: 'DiagramPerformanceWarningRoot',
|
||||
render(createElement) {
|
||||
return createElement(DiagramPerformanceWarning, {
|
||||
on: {
|
||||
closeAlert() {
|
||||
app.$destroy();
|
||||
app.$el.remove();
|
||||
},
|
||||
showImage() {
|
||||
krokiImage.removeAttribute('hidden');
|
||||
app.$destroy();
|
||||
app.$el.remove();
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return app.$el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add warning alert to hidden Kroki images,
|
||||
* or show Kroki image if on an unrestricted page.
|
||||
*
|
||||
* Kroki images are given a hidden attribute by the
|
||||
* backend when the original markdown source is large.
|
||||
*
|
||||
* @param {Array<Element>} krokiImages Array of hidden Kroki `img` elements
|
||||
*/
|
||||
export function renderKroki(krokiImages) {
|
||||
const pageName = document.querySelector('body').dataset.page;
|
||||
const isUnrestrictedPage = unrestrictedPages.includes(pageName);
|
||||
|
||||
krokiImages.forEach((krokiImage) => {
|
||||
if (isUnrestrictedPage) {
|
||||
krokiImage.removeAttribute('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = krokiImage.parentElement;
|
||||
|
||||
// A single Kroki image is processed multiple times for some reason,
|
||||
// so this condition ensures we only create one alert per Kroki image
|
||||
if (!parent.hasAttribute('data-kroki-processed')) {
|
||||
parent.setAttribute('data-kroki-processed', 'true');
|
||||
parent.after(createAlert(krokiImage));
|
||||
}
|
||||
});
|
||||
}
|
|
@ -3,6 +3,7 @@ import { once, countBy } from 'lodash';
|
|||
import createFlash from '~/flash';
|
||||
import { darkModeEnabled } from '~/lib/utils/color_utils';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { unrestrictedPages } from './constants';
|
||||
|
||||
// Renders diagrams and flowcharts from text using Mermaid in any element with the
|
||||
// `js-render-mermaid` class.
|
||||
|
@ -30,24 +31,6 @@ let renderedMermaidBlocks = 0;
|
|||
|
||||
let mermaidModule = {};
|
||||
|
||||
// Whitelist pages where we won't impose any restrictions
|
||||
// on mermaid rendering
|
||||
const WHITELISTED_PAGES = [
|
||||
// Group wiki
|
||||
'groups:wikis:show',
|
||||
'groups:wikis:edit',
|
||||
'groups:wikis:create',
|
||||
|
||||
// Project wiki
|
||||
'projects:wikis:show',
|
||||
'projects:wikis:edit',
|
||||
'projects:wikis:create',
|
||||
|
||||
// Project files
|
||||
'projects:show',
|
||||
'projects:blob:show',
|
||||
];
|
||||
|
||||
export function initMermaid(mermaid) {
|
||||
let theme = 'neutral';
|
||||
|
||||
|
@ -163,7 +146,7 @@ function renderMermaids($els) {
|
|||
* up the entire thread and causing a DoS.
|
||||
*/
|
||||
if (
|
||||
!WHITELISTED_PAGES.includes(pageName) &&
|
||||
!unrestrictedPages.includes(pageName) &&
|
||||
((source && source.length > MAX_CHAR_LIMIT) ||
|
||||
renderedChars > MAX_CHAR_LIMIT ||
|
||||
renderedMermaidBlocks >= MAX_MERMAID_BLOCK_LIMIT ||
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from '~/lib/utils/url_utility';
|
||||
import { darkModeEnabled } from '~/lib/utils/color_utils';
|
||||
import { setAttributes } from '~/lib/utils/dom_utils';
|
||||
import { unrestrictedPages } from './constants';
|
||||
|
||||
// Renders diagrams and flowcharts from text using Mermaid in any element with the
|
||||
// `js-render-mermaid` class.
|
||||
|
@ -36,23 +37,6 @@ const BUFFER_IFRAME_HEIGHT = 10;
|
|||
const elsProcessingMap = new WeakMap();
|
||||
let renderedMermaidBlocks = 0;
|
||||
|
||||
// Pages without any restrictions on mermaid rendering
|
||||
const PAGES_WITHOUT_RESTRICTIONS = [
|
||||
// Group wiki
|
||||
'groups:wikis:show',
|
||||
'groups:wikis:edit',
|
||||
'groups:wikis:create',
|
||||
|
||||
// Project wiki
|
||||
'projects:wikis:show',
|
||||
'projects:wikis:edit',
|
||||
'projects:wikis:create',
|
||||
|
||||
// Project files
|
||||
'projects:show',
|
||||
'projects:blob:show',
|
||||
];
|
||||
|
||||
function shouldLazyLoadMermaidBlock(source) {
|
||||
/**
|
||||
* If source contains `&`, which means that it might
|
||||
|
@ -149,7 +133,7 @@ function renderMermaids($els) {
|
|||
* up the entire thread and causing a DoS.
|
||||
*/
|
||||
if (
|
||||
!PAGES_WITHOUT_RESTRICTIONS.includes(pageName) &&
|
||||
!unrestrictedPages.includes(pageName) &&
|
||||
((source && source.length > MAX_CHAR_LIMIT) ||
|
||||
renderedChars > MAX_CHAR_LIMIT ||
|
||||
renderedMermaidBlocks >= MAX_MERMAID_BLOCK_LIMIT ||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { SwaggerUIBundle } from 'swagger-ui-dist';
|
||||
import createFlash from '~/flash';
|
||||
import { removeParams, updateHistory } from '~/lib/utils/url_utility';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default () => {
|
||||
|
@ -8,14 +7,10 @@ export default () => {
|
|||
|
||||
Promise.all([import(/* webpackChunkName: 'openapi' */ 'swagger-ui-dist/swagger-ui.css')])
|
||||
.then(() => {
|
||||
// Temporary fix to prevent an XSS attack due to "useUnsafeMarkdown"
|
||||
// Once we upgrade Swagger to "4.0.0", we can safely remove this as it will be deprecated
|
||||
// Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/339696
|
||||
updateHistory({ url: removeParams(['useUnsafeMarkdown']), replace: true });
|
||||
SwaggerUIBundle({
|
||||
url: el.dataset.endpoint,
|
||||
dom_id: '#js-openapi-viewer',
|
||||
useUnsafeMarkdown: false,
|
||||
deepLinking: true,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
|
|
|
@ -1,224 +0,0 @@
|
|||
<script>
|
||||
import { GlAlert, GlButton, GlDrawer, GlFormGroup, GlFormInput } from '@gitlab/ui';
|
||||
import { produce } from 'immer';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { TYPE_GROUP } from '~/graphql_shared/constants';
|
||||
import createContactMutation from './queries/create_contact.mutation.graphql';
|
||||
import updateContactMutation from './queries/update_contact.mutation.graphql';
|
||||
import getGroupContactsQuery from './queries/get_group_contacts.query.graphql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlDrawer,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
},
|
||||
inject: ['groupFullPath', 'groupId'],
|
||||
props: {
|
||||
drawerOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
contact: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
description: '',
|
||||
submitting: false,
|
||||
errorMessages: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
invalid() {
|
||||
const { firstName, lastName, email } = this;
|
||||
|
||||
return firstName.trim() === '' || lastName.trim() === '' || email.trim() === '';
|
||||
},
|
||||
editMode() {
|
||||
return Boolean(this.contact);
|
||||
},
|
||||
title() {
|
||||
return this.editMode ? this.$options.i18n.editTitle : this.$options.i18n.newTitle;
|
||||
},
|
||||
buttonLabel() {
|
||||
return this.editMode
|
||||
? this.$options.i18n.editButtonLabel
|
||||
: this.$options.i18n.createButtonLabel;
|
||||
},
|
||||
mutation() {
|
||||
return this.editMode ? updateContactMutation : createContactMutation;
|
||||
},
|
||||
variables() {
|
||||
const { contact, firstName, lastName, phone, email, description, editMode, groupId } = this;
|
||||
|
||||
const variables = {
|
||||
input: {
|
||||
firstName,
|
||||
lastName,
|
||||
phone,
|
||||
email,
|
||||
description,
|
||||
},
|
||||
};
|
||||
|
||||
if (editMode) {
|
||||
variables.input.id = contact.id;
|
||||
} else {
|
||||
variables.input.groupId = convertToGraphQLId(TYPE_GROUP, groupId);
|
||||
}
|
||||
|
||||
return variables;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.editMode) {
|
||||
const { contact } = this;
|
||||
|
||||
this.firstName = contact.firstName || '';
|
||||
this.lastName = contact.lastName || '';
|
||||
this.phone = contact.phone || '';
|
||||
this.email = contact.email || '';
|
||||
this.description = contact.description || '';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
const { mutation, variables, updateCache, close } = this;
|
||||
|
||||
this.submitting = true;
|
||||
|
||||
return this.$apollo
|
||||
.mutate({
|
||||
mutation,
|
||||
variables,
|
||||
update: updateCache,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (
|
||||
data.customerRelationsContactCreate?.errors.length === 0 ||
|
||||
data.customerRelationsContactUpdate?.errors.length === 0
|
||||
) {
|
||||
close(true);
|
||||
}
|
||||
|
||||
this.submitting = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.errorMessages = [this.$options.i18n.somethingWentWrong];
|
||||
this.submitting = false;
|
||||
});
|
||||
},
|
||||
close(success) {
|
||||
this.$emit('close', success);
|
||||
},
|
||||
updateCache(store, { data }) {
|
||||
const mutationData =
|
||||
data.customerRelationsContactCreate || data.customerRelationsContactUpdate;
|
||||
|
||||
if (mutationData?.errors.length > 0) {
|
||||
this.errorMessages = mutationData.errors;
|
||||
return;
|
||||
}
|
||||
|
||||
const queryArgs = {
|
||||
query: getGroupContactsQuery,
|
||||
variables: { groupFullPath: this.groupFullPath },
|
||||
};
|
||||
|
||||
const sourceData = store.readQuery(queryArgs);
|
||||
|
||||
queryArgs.data = produce(sourceData, (draftState) => {
|
||||
draftState.group.contacts.nodes = [
|
||||
...sourceData.group.contacts.nodes.filter(({ id }) => id !== this.contact?.id),
|
||||
mutationData.contact,
|
||||
];
|
||||
});
|
||||
|
||||
store.writeQuery(queryArgs);
|
||||
},
|
||||
getDrawerHeaderHeight() {
|
||||
const wrapperEl = document.querySelector('.content-wrapper');
|
||||
|
||||
if (wrapperEl) {
|
||||
return `${wrapperEl.offsetTop}px`;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
createButtonLabel: s__('Crm|Create new contact'),
|
||||
editButtonLabel: __('Save changes'),
|
||||
cancel: __('Cancel'),
|
||||
firstName: s__('Crm|First name'),
|
||||
lastName: s__('Crm|Last name'),
|
||||
email: s__('Crm|Email'),
|
||||
phone: s__('Crm|Phone number (optional)'),
|
||||
description: s__('Crm|Description (optional)'),
|
||||
newTitle: s__('Crm|New contact'),
|
||||
editTitle: s__('Crm|Edit contact'),
|
||||
somethingWentWrong: __('Something went wrong. Please try again.'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-drawer
|
||||
class="gl-drawer-responsive"
|
||||
:open="drawerOpen"
|
||||
:header-height="getDrawerHeaderHeight()"
|
||||
@close="close(false)"
|
||||
>
|
||||
<template #title>
|
||||
<h3>{{ title }}</h3>
|
||||
</template>
|
||||
<gl-alert v-if="errorMessages.length" variant="danger" @dismiss="errorMessages = []">
|
||||
<ul class="gl-mb-0! gl-ml-5">
|
||||
<li v-for="error in errorMessages" :key="error">
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</gl-alert>
|
||||
<form @submit.prevent="save">
|
||||
<gl-form-group :label="$options.i18n.firstName" label-for="contact-first-name">
|
||||
<gl-form-input id="contact-first-name" v-model="firstName" />
|
||||
</gl-form-group>
|
||||
<gl-form-group :label="$options.i18n.lastName" label-for="contact-last-name">
|
||||
<gl-form-input id="contact-last-name" v-model="lastName" />
|
||||
</gl-form-group>
|
||||
<gl-form-group :label="$options.i18n.email" label-for="contact-email">
|
||||
<gl-form-input id="contact-email" v-model="email" />
|
||||
</gl-form-group>
|
||||
<gl-form-group :label="$options.i18n.phone" label-for="contact-phone">
|
||||
<gl-form-input id="contact-phone" v-model="phone" />
|
||||
</gl-form-group>
|
||||
<gl-form-group :label="$options.i18n.description" label-for="contact-description">
|
||||
<gl-form-input id="contact-description" v-model="description" />
|
||||
</gl-form-group>
|
||||
<span class="gl-float-right">
|
||||
<gl-button data-testid="cancel-button" @click="close(false)">
|
||||
{{ $options.i18n.cancel }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
variant="confirm"
|
||||
:disabled="invalid"
|
||||
:loading="submitting"
|
||||
data-testid="save-contact-button"
|
||||
type="submit"
|
||||
>{{ buttonLabel }}</gl-button
|
||||
>
|
||||
</span>
|
||||
</form>
|
||||
</gl-drawer>
|
||||
</template>
|
|
@ -61,11 +61,6 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
existingModel: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
additionalCreateParams: {
|
||||
type: Object,
|
||||
required: false,
|
||||
|
@ -76,25 +71,42 @@ export default {
|
|||
required: false,
|
||||
default: () => MSG_SAVE_CHANGES,
|
||||
},
|
||||
existingId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const initialModel = this.fields.reduce(
|
||||
(map, field) =>
|
||||
Object.assign(map, {
|
||||
[field.name]: this.existingModel ? this.existingModel[field.name] : null,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
return {
|
||||
model: initialModel,
|
||||
model: null,
|
||||
submitting: false,
|
||||
errorMessages: [],
|
||||
records: [],
|
||||
loading: true,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
records: {
|
||||
query() {
|
||||
return this.getQuery.query;
|
||||
},
|
||||
variables() {
|
||||
return this.getQuery.variables;
|
||||
},
|
||||
update(data) {
|
||||
this.records = getPropValueByPath(data, this.getQueryNodePath).nodes || [];
|
||||
this.setInitialModel();
|
||||
this.loading = false;
|
||||
},
|
||||
error() {
|
||||
this.errorMessages = [MSG_ERROR];
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isEditMode() {
|
||||
return this.existingModel?.id;
|
||||
return this.existingId;
|
||||
},
|
||||
isInvalid() {
|
||||
const { fields, model } = this;
|
||||
|
@ -115,13 +127,24 @@ export default {
|
|||
);
|
||||
|
||||
if (isEditMode) {
|
||||
return { input: { id: this.existingModel.id, ...variables } };
|
||||
return { input: { id: this.existingId, ...variables } };
|
||||
}
|
||||
|
||||
return { input: { ...additionalCreateParams, ...variables } };
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setInitialModel() {
|
||||
const existingModel = this.records.find(({ id }) => id === this.existingId);
|
||||
|
||||
this.model = this.fields.reduce(
|
||||
(map, field) =>
|
||||
Object.assign(map, {
|
||||
[field.name]: !this.isEditMode || !existingModel ? null : existingModel[field.name],
|
||||
}),
|
||||
{},
|
||||
);
|
||||
},
|
||||
formatValue(model, field) {
|
||||
if (!isEmpty(model[field.name]) && field.input?.type === 'number') {
|
||||
return parseFloat(model[field.name]);
|
||||
|
@ -173,7 +196,7 @@ export default {
|
|||
const sourceData = store.readQuery(getQuery);
|
||||
|
||||
const newData = produce(sourceData, (draftState) => {
|
||||
getPropValueByPath(draftState, getQueryNodePath).nodes.push(getFirstPropertyValue(result));
|
||||
getPropValueByPath(draftState, getQueryNodePath).nodes.push(this.getPayload(result));
|
||||
});
|
||||
|
||||
store.writeQuery({
|
||||
|
@ -185,6 +208,14 @@ export default {
|
|||
const optionalSuffix = field.required ? '' : ` ${MSG_OPTIONAL}`;
|
||||
return field.label + optionalSuffix;
|
||||
},
|
||||
getPayload(data) {
|
||||
if (!data) return null;
|
||||
|
||||
const keys = Object.keys(data);
|
||||
if (keys[0] === '__typename') return data[keys[1]];
|
||||
|
||||
return data[keys[0]];
|
||||
},
|
||||
},
|
||||
MSG_CANCEL,
|
||||
INDEX_ROUTE_NAME,
|
||||
|
@ -192,7 +223,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<mounting-portal mount-to="#js-crm-form-portal" append>
|
||||
<mounting-portal v-if="!loading" mount-to="#js-crm-form-portal" append>
|
||||
<gl-drawer class="gl-drawer-responsive gl-absolute" :open="drawerOpen" @close="close(false)">
|
||||
<template #title>
|
||||
<h3>{{ title }}</h3>
|
||||
|
|
|
@ -1,164 +0,0 @@
|
|||
<script>
|
||||
import { GlAlert, GlButton, GlDrawer, GlFormGroup, GlFormInput } from '@gitlab/ui';
|
||||
import { produce } from 'immer';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { TYPE_GROUP } from '~/graphql_shared/constants';
|
||||
import createOrganization from './queries/create_organization.mutation.graphql';
|
||||
import getGroupOrganizationsQuery from './queries/get_group_organizations.query.graphql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlDrawer,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
},
|
||||
inject: ['groupFullPath', 'groupId'],
|
||||
props: {
|
||||
drawerOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
defaultRate: null,
|
||||
description: '',
|
||||
submitting: false,
|
||||
errorMessages: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
invalid() {
|
||||
return this.name.trim() === '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
this.submitting = true;
|
||||
return this.$apollo
|
||||
.mutate({
|
||||
mutation: createOrganization,
|
||||
variables: {
|
||||
input: {
|
||||
groupId: convertToGraphQLId(TYPE_GROUP, this.groupId),
|
||||
name: this.name,
|
||||
defaultRate: this.defaultRate ? parseFloat(this.defaultRate) : null,
|
||||
description: this.description,
|
||||
},
|
||||
},
|
||||
update: this.updateCache,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.customerRelationsOrganizationCreate.errors.length === 0) this.close(true);
|
||||
|
||||
this.submitting = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.errorMessages = [this.$options.i18n.somethingWentWrong];
|
||||
this.submitting = false;
|
||||
});
|
||||
},
|
||||
close(success) {
|
||||
this.$emit('close', success);
|
||||
},
|
||||
updateCache(store, { data: { customerRelationsOrganizationCreate } }) {
|
||||
if (customerRelationsOrganizationCreate.errors.length > 0) {
|
||||
this.errorMessages = customerRelationsOrganizationCreate.errors;
|
||||
return;
|
||||
}
|
||||
|
||||
const variables = {
|
||||
groupFullPath: this.groupFullPath,
|
||||
};
|
||||
const sourceData = store.readQuery({
|
||||
query: getGroupOrganizationsQuery,
|
||||
variables,
|
||||
});
|
||||
|
||||
const data = produce(sourceData, (draftState) => {
|
||||
draftState.group.organizations.nodes = [
|
||||
...sourceData.group.organizations.nodes,
|
||||
customerRelationsOrganizationCreate.organization,
|
||||
];
|
||||
});
|
||||
|
||||
store.writeQuery({
|
||||
query: getGroupOrganizationsQuery,
|
||||
variables,
|
||||
data,
|
||||
});
|
||||
},
|
||||
getDrawerHeaderHeight() {
|
||||
const wrapperEl = document.querySelector('.content-wrapper');
|
||||
|
||||
if (wrapperEl) {
|
||||
return `${wrapperEl.offsetTop}px`;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
buttonLabel: s__('Crm|Create organization'),
|
||||
cancel: __('Cancel'),
|
||||
name: __('Name'),
|
||||
defaultRate: s__('Crm|Default rate (optional)'),
|
||||
description: __('Description (optional)'),
|
||||
title: s__('Crm|New Organization'),
|
||||
somethingWentWrong: __('Something went wrong. Please try again.'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-drawer
|
||||
class="gl-drawer-responsive"
|
||||
:open="drawerOpen"
|
||||
:header-height="getDrawerHeaderHeight()"
|
||||
@close="close(false)"
|
||||
>
|
||||
<template #title>
|
||||
<h4>{{ $options.i18n.title }}</h4>
|
||||
</template>
|
||||
<gl-alert v-if="errorMessages.length" variant="danger" @dismiss="errorMessages = []">
|
||||
<ul class="gl-mb-0! gl-ml-5">
|
||||
<li v-for="error in errorMessages" :key="error">
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</gl-alert>
|
||||
<form @submit.prevent="save">
|
||||
<gl-form-group :label="$options.i18n.name" label-for="organization-name">
|
||||
<gl-form-input id="organization-name" v-model="name" />
|
||||
</gl-form-group>
|
||||
<gl-form-group :label="$options.i18n.defaultRate" label-for="organization-default-rate">
|
||||
<gl-form-input
|
||||
id="organization-default-rate"
|
||||
v-model="defaultRate"
|
||||
type="number"
|
||||
step="0.01"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group :label="$options.i18n.description" label-for="organization-description">
|
||||
<gl-form-input id="organization-description" v-model="description" />
|
||||
</gl-form-group>
|
||||
<span class="gl-float-right">
|
||||
<gl-button data-testid="cancel-button" @click="close(false)">
|
||||
{{ $options.i18n.cancel }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
variant="confirm"
|
||||
:disabled="invalid"
|
||||
:loading="submitting"
|
||||
data-testid="create-new-organization-button"
|
||||
type="submit"
|
||||
>{{ $options.i18n.buttonLabel }}</gl-button
|
||||
>
|
||||
</span>
|
||||
</form>
|
||||
</gl-drawer>
|
||||
</template>
|
|
@ -0,0 +1,78 @@
|
|||
<script>
|
||||
import { s__, __ } from '~/locale';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { TYPE_CRM_CONTACT, TYPE_GROUP } from '~/graphql_shared/constants';
|
||||
import ContactForm from '../../components/form.vue';
|
||||
import getGroupContactsQuery from './graphql/get_group_contacts.query.graphql';
|
||||
import createContactMutation from './graphql/create_contact.mutation.graphql';
|
||||
import updateContactMutation from './graphql/update_contact.mutation.graphql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContactForm,
|
||||
},
|
||||
inject: ['groupFullPath', 'groupId'],
|
||||
props: {
|
||||
isEditMode: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
contactGraphQLId() {
|
||||
if (!this.isEditMode) return null;
|
||||
|
||||
return convertToGraphQLId(TYPE_CRM_CONTACT, this.$route.params.id);
|
||||
},
|
||||
groupGraphQLId() {
|
||||
return convertToGraphQLId(TYPE_GROUP, this.groupId);
|
||||
},
|
||||
mutation() {
|
||||
if (this.isEditMode) return updateContactMutation;
|
||||
|
||||
return createContactMutation;
|
||||
},
|
||||
getQuery() {
|
||||
return {
|
||||
query: getGroupContactsQuery,
|
||||
variables: { groupFullPath: this.groupFullPath },
|
||||
};
|
||||
},
|
||||
title() {
|
||||
if (this.isEditMode) return s__('Crm|Edit contact');
|
||||
|
||||
return s__('Crm|New contact');
|
||||
},
|
||||
successMessage() {
|
||||
if (this.isEditMode) return s__('Crm|Contact has been updated.');
|
||||
|
||||
return s__('Crm|Contact has been added.');
|
||||
},
|
||||
additionalCreateParams() {
|
||||
return { groupId: this.groupGraphQLId };
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{ name: 'firstName', label: __('First name'), required: true },
|
||||
{ name: 'lastName', label: __('Last name'), required: true },
|
||||
{ name: 'email', label: __('Email'), required: true },
|
||||
{ name: 'phone', label: __('Phone') },
|
||||
{ name: 'description', label: __('Description') },
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<contact-form
|
||||
:drawer-open="true"
|
||||
:get-query="getQuery"
|
||||
get-query-node-path="group.contacts"
|
||||
:mutation="mutation"
|
||||
:additional-create-params="additionalCreateParams"
|
||||
:existing-id="contactGraphQLId"
|
||||
:fields="$options.fields"
|
||||
:title="title"
|
||||
:success-message="successMessage"
|
||||
/>
|
||||
</template>
|
|
@ -2,11 +2,9 @@
|
|||
import { GlAlert, GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import { s__, __ } from '~/locale';
|
||||
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { TYPE_CRM_CONTACT } from '~/graphql_shared/constants';
|
||||
import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from '../constants';
|
||||
import getGroupContactsQuery from './queries/get_group_contacts.query.graphql';
|
||||
import ContactForm from './contact_form.vue';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { EDIT_ROUTE_NAME, NEW_ROUTE_NAME } from '../../constants';
|
||||
import getGroupContactsQuery from './graphql/get_group_contacts.query.graphql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -14,12 +12,11 @@ export default {
|
|||
GlButton,
|
||||
GlLoadingIcon,
|
||||
GlTable,
|
||||
ContactForm,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
inject: ['groupFullPath', 'groupIssuesPath', 'canAdminCrmContact'],
|
||||
inject: ['canAdminCrmContact', 'groupFullPath', 'groupIssuesPath'],
|
||||
data() {
|
||||
return {
|
||||
contacts: [],
|
||||
|
@ -48,50 +45,20 @@ export default {
|
|||
isLoading() {
|
||||
return this.$apollo.queries.contacts.loading;
|
||||
},
|
||||
showNewForm() {
|
||||
return this.$route.name === NEW_ROUTE_NAME;
|
||||
},
|
||||
showEditForm() {
|
||||
return !this.isLoading && this.$route.name === EDIT_ROUTE_NAME;
|
||||
},
|
||||
canAdmin() {
|
||||
return parseBoolean(this.canAdminCrmContact);
|
||||
},
|
||||
editingContact() {
|
||||
return this.contacts.find(
|
||||
(contact) => contact.id === convertToGraphQLId(TYPE_CRM_CONTACT, this.$route.params.id),
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
extractContacts(data) {
|
||||
const contacts = data?.group?.contacts?.nodes || [];
|
||||
return contacts.slice().sort((a, b) => a.firstName.localeCompare(b.firstName));
|
||||
},
|
||||
displayNewForm() {
|
||||
if (this.showNewForm) return;
|
||||
|
||||
this.$router.push({ name: NEW_ROUTE_NAME });
|
||||
},
|
||||
hideNewForm(success) {
|
||||
if (success) this.$toast.show(s__('Crm|Contact has been added'));
|
||||
|
||||
this.$router.replace({ name: INDEX_ROUTE_NAME });
|
||||
},
|
||||
hideEditForm(success) {
|
||||
if (success) this.$toast.show(s__('Crm|Contact has been updated'));
|
||||
|
||||
this.editingContactId = 0;
|
||||
this.$router.replace({ name: INDEX_ROUTE_NAME });
|
||||
},
|
||||
getIssuesPath(path, value) {
|
||||
return `${path}?scope=all&state=opened&crm_contact_id=${value}`;
|
||||
},
|
||||
edit(value) {
|
||||
if (this.showEditForm) return;
|
||||
|
||||
this.editingContactId = value;
|
||||
this.$router.push({ name: EDIT_ROUTE_NAME, params: { id: value } });
|
||||
getEditRoute(id) {
|
||||
return { name: this.$options.EDIT_ROUTE_NAME, params: { id } };
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
|
@ -119,10 +86,12 @@ export default {
|
|||
emptyText: s__('Crm|No contacts found'),
|
||||
issuesButtonLabel: __('View issues'),
|
||||
editButtonLabel: __('Edit'),
|
||||
title: s__('Crm|Customer Relations Contacts'),
|
||||
title: s__('Crm|Customer relations contacts'),
|
||||
newContact: s__('Crm|New contact'),
|
||||
errorText: __('Something went wrong. Please try again.'),
|
||||
},
|
||||
EDIT_ROUTE_NAME,
|
||||
NEW_ROUTE_NAME,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -137,24 +106,15 @@ export default {
|
|||
<h2 class="gl-font-size-h2 gl-my-0">
|
||||
{{ $options.i18n.title }}
|
||||
</h2>
|
||||
<div class="gl-display-none gl-md-display-flex gl-align-items-center gl-justify-content-end">
|
||||
<gl-button
|
||||
v-if="canAdmin"
|
||||
variant="confirm"
|
||||
data-testid="new-contact-button"
|
||||
@click="displayNewForm"
|
||||
>
|
||||
<div v-if="canAdmin">
|
||||
<router-link :to="{ name: $options.NEW_ROUTE_NAME }">
|
||||
<gl-button variant="confirm" data-testid="new-contact-button">
|
||||
{{ $options.i18n.newContact }}
|
||||
</gl-button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<contact-form v-if="showNewForm" :drawer-open="showNewForm" @close="hideNewForm" />
|
||||
<contact-form
|
||||
v-if="showEditForm"
|
||||
:contact="editingContact"
|
||||
:drawer-open="showEditForm"
|
||||
@close="hideEditForm"
|
||||
/>
|
||||
<router-view />
|
||||
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
|
||||
<gl-table
|
||||
v-else
|
||||
|
@ -164,23 +124,24 @@ export default {
|
|||
:empty-text="$options.i18n.emptyText"
|
||||
show-empty
|
||||
>
|
||||
<template #cell(id)="data">
|
||||
<template #cell(id)="{ value: id }">
|
||||
<gl-button
|
||||
v-gl-tooltip.hover.bottom="$options.i18n.issuesButtonLabel"
|
||||
class="gl-mr-3"
|
||||
data-testid="issues-link"
|
||||
icon="issues"
|
||||
:aria-label="$options.i18n.issuesButtonLabel"
|
||||
:href="getIssuesPath(groupIssuesPath, data.value)"
|
||||
:href="getIssuesPath(groupIssuesPath, id)"
|
||||
/>
|
||||
<router-link :to="getEditRoute(id)">
|
||||
<gl-button
|
||||
v-if="canAdmin"
|
||||
v-gl-tooltip.hover.bottom="$options.i18n.editButtonLabel"
|
||||
data-testid="edit-contact-button"
|
||||
icon="pencil"
|
||||
:aria-label="$options.i18n.editButtonLabel"
|
||||
@click="edit(data.value)"
|
||||
/>
|
||||
</router-link>
|
||||
</template>
|
||||
</gl-table>
|
||||
</div>
|
|
@ -1,4 +1,5 @@
|
|||
import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from './constants';
|
||||
import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from '../constants';
|
||||
import ContactFormWrapper from './components/contact_form_wrapper.vue';
|
||||
|
||||
export default [
|
||||
{
|
||||
|
@ -8,9 +9,12 @@ export default [
|
|||
{
|
||||
name: NEW_ROUTE_NAME,
|
||||
path: '/new',
|
||||
component: ContactFormWrapper,
|
||||
},
|
||||
{
|
||||
name: EDIT_ROUTE_NAME,
|
||||
path: '/:id/edit',
|
||||
component: ContactFormWrapper,
|
||||
props: { isEditMode: true },
|
||||
},
|
||||
];
|
|
@ -0,0 +1,10 @@
|
|||
#import "./crm_organization_fields.fragment.graphql"
|
||||
|
||||
mutation updateOrganization($input: CustomerRelationsOrganizationUpdateInput!) {
|
||||
customerRelationsOrganizationUpdate(input: $input) {
|
||||
organization {
|
||||
...OrganizationFragment
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
<script>
|
||||
import { s__, __ } from '~/locale';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { TYPE_CRM_ORGANIZATION, TYPE_GROUP } from '~/graphql_shared/constants';
|
||||
import OrganizationForm from '../../components/form.vue';
|
||||
import getGroupOrganizationsQuery from './graphql/get_group_organizations.query.graphql';
|
||||
import createOrganizationMutation from './graphql/create_organization.mutation.graphql';
|
||||
import updateOrganizationMutation from './graphql/update_organization.mutation.graphql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
OrganizationForm,
|
||||
},
|
||||
inject: ['groupFullPath', 'groupId'],
|
||||
props: {
|
||||
isEditMode: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
organizationGraphQLId() {
|
||||
if (!this.isEditMode) return null;
|
||||
|
||||
return convertToGraphQLId(TYPE_CRM_ORGANIZATION, this.$route.params.id);
|
||||
},
|
||||
groupGraphQLId() {
|
||||
return convertToGraphQLId(TYPE_GROUP, this.groupId);
|
||||
},
|
||||
mutation() {
|
||||
if (this.isEditMode) return updateOrganizationMutation;
|
||||
|
||||
return createOrganizationMutation;
|
||||
},
|
||||
getQuery() {
|
||||
return {
|
||||
query: getGroupOrganizationsQuery,
|
||||
variables: { groupFullPath: this.groupFullPath },
|
||||
};
|
||||
},
|
||||
title() {
|
||||
if (this.isEditMode) return s__('Crm|Edit organization');
|
||||
|
||||
return s__('Crm|New organization');
|
||||
},
|
||||
successMessage() {
|
||||
if (this.isEditMode) return s__('Crm|Organization has been updated.');
|
||||
|
||||
return s__('Crm|Organization has been added.');
|
||||
},
|
||||
additionalCreateParams() {
|
||||
return { groupId: this.groupGraphQLId };
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{ name: 'name', label: __('Name'), required: true },
|
||||
{
|
||||
name: 'defaultRate',
|
||||
label: s__('Crm|Default rate'),
|
||||
input: { type: 'number', step: '0.01' },
|
||||
},
|
||||
{ name: 'description', label: __('Description') },
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<organization-form
|
||||
:drawer-open="true"
|
||||
:get-query="getQuery"
|
||||
get-query-node-path="group.organizations"
|
||||
:mutation="mutation"
|
||||
:additional-create-params="additionalCreateParams"
|
||||
:existing-id="organizationGraphQLId"
|
||||
:fields="$options.fields"
|
||||
:title="title"
|
||||
:success-message="successMessage"
|
||||
/>
|
||||
</template>
|
|
@ -3,9 +3,8 @@ import { GlAlert, GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@
|
|||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import { s__, __ } from '~/locale';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME } from '../constants';
|
||||
import getGroupOrganizationsQuery from './queries/get_group_organizations.query.graphql';
|
||||
import NewOrganizationForm from './new_organization_form.vue';
|
||||
import { EDIT_ROUTE_NAME, NEW_ROUTE_NAME } from '../../constants';
|
||||
import getGroupOrganizationsQuery from './graphql/get_group_organizations.query.graphql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -13,7 +12,6 @@ export default {
|
|||
GlButton,
|
||||
GlLoadingIcon,
|
||||
GlTable,
|
||||
NewOrganizationForm,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
@ -21,8 +19,8 @@ export default {
|
|||
inject: ['canAdminCrmOrganization', 'groupFullPath', 'groupIssuesPath'],
|
||||
data() {
|
||||
return {
|
||||
error: false,
|
||||
organizations: [],
|
||||
error: false,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
|
@ -47,10 +45,7 @@ export default {
|
|||
isLoading() {
|
||||
return this.$apollo.queries.organizations.loading;
|
||||
},
|
||||
showNewForm() {
|
||||
return this.$route.name === NEW_ROUTE_NAME;
|
||||
},
|
||||
canCreateNew() {
|
||||
canAdmin() {
|
||||
return parseBoolean(this.canAdminCrmOrganization);
|
||||
},
|
||||
},
|
||||
|
@ -62,15 +57,8 @@ export default {
|
|||
getIssuesPath(path, value) {
|
||||
return `${path}?scope=all&state=opened&crm_organization_id=${value}`;
|
||||
},
|
||||
displayNewForm() {
|
||||
if (this.showNewForm) return;
|
||||
|
||||
this.$router.push({ name: NEW_ROUTE_NAME });
|
||||
},
|
||||
hideNewForm(success) {
|
||||
if (success) this.$toast.show(this.$options.i18n.organizationAdded);
|
||||
|
||||
this.$router.replace({ name: INDEX_ROUTE_NAME });
|
||||
getEditRoute(id) {
|
||||
return { name: this.$options.EDIT_ROUTE_NAME, params: { id } };
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
|
@ -79,7 +67,7 @@ export default {
|
|||
{ key: 'description', sortable: true },
|
||||
{
|
||||
key: 'id',
|
||||
label: __('Issues'),
|
||||
label: '',
|
||||
formatter: (id) => {
|
||||
return getIdFromGraphQLId(id);
|
||||
},
|
||||
|
@ -88,11 +76,13 @@ export default {
|
|||
i18n: {
|
||||
emptyText: s__('Crm|No organizations found'),
|
||||
issuesButtonLabel: __('View issues'),
|
||||
title: s__('Crm|Customer Relations Organizations'),
|
||||
editButtonLabel: __('Edit'),
|
||||
title: s__('Crm|Customer relations organizations'),
|
||||
newOrganization: s__('Crm|New organization'),
|
||||
errorText: __('Something went wrong. Please try again.'),
|
||||
organizationAdded: s__('Crm|Organization has been added'),
|
||||
},
|
||||
EDIT_ROUTE_NAME,
|
||||
NEW_ROUTE_NAME,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -108,15 +98,17 @@ export default {
|
|||
{{ $options.i18n.title }}
|
||||
</h2>
|
||||
<div
|
||||
v-if="canCreateNew"
|
||||
v-if="canAdmin"
|
||||
class="gl-display-none gl-md-display-flex gl-align-items-center gl-justify-content-end"
|
||||
>
|
||||
<gl-button variant="confirm" data-testid="new-organization-button" @click="displayNewForm">
|
||||
<router-link :to="{ name: $options.NEW_ROUTE_NAME }">
|
||||
<gl-button variant="confirm" data-testid="new-organization-button">
|
||||
{{ $options.i18n.newOrganization }}
|
||||
</gl-button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<new-organization-form v-if="showNewForm" :drawer-open="showNewForm" @close="hideNewForm" />
|
||||
<router-view />
|
||||
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
|
||||
<gl-table
|
||||
v-else
|
||||
|
@ -126,14 +118,24 @@ export default {
|
|||
:empty-text="$options.i18n.emptyText"
|
||||
show-empty
|
||||
>
|
||||
<template #cell(id)="data">
|
||||
<template #cell(id)="{ value: id }">
|
||||
<gl-button
|
||||
v-gl-tooltip.hover.bottom="$options.i18n.issuesButtonLabel"
|
||||
class="gl-mr-3"
|
||||
data-testid="issues-link"
|
||||
icon="issues"
|
||||
:aria-label="$options.i18n.issuesButtonLabel"
|
||||
:href="getIssuesPath(groupIssuesPath, data.value)"
|
||||
:href="getIssuesPath(groupIssuesPath, id)"
|
||||
/>
|
||||
<router-link :to="getEditRoute(id)">
|
||||
<gl-button
|
||||
v-if="canAdmin"
|
||||
v-gl-tooltip.hover.bottom="$options.i18n.editButtonLabel"
|
||||
data-testid="edit-organization-button"
|
||||
icon="pencil"
|
||||
:aria-label="$options.i18n.editButtonLabel"
|
||||
/>
|
||||
</router-link>
|
||||
</template>
|
||||
</gl-table>
|
||||
</div>
|
20
app/assets/javascripts/crm/organizations/routes.js
Normal file
20
app/assets/javascripts/crm/organizations/routes.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from '../constants';
|
||||
import OrganizationFormWrapper from './components/organization_form_wrapper.vue';
|
||||
|
||||
export default [
|
||||
{
|
||||
name: INDEX_ROUTE_NAME,
|
||||
path: '/',
|
||||
},
|
||||
{
|
||||
name: NEW_ROUTE_NAME,
|
||||
path: '/new',
|
||||
component: OrganizationFormWrapper,
|
||||
},
|
||||
{
|
||||
name: EDIT_ROUTE_NAME,
|
||||
path: '/:id/edit',
|
||||
component: OrganizationFormWrapper,
|
||||
props: { isEditMode: true },
|
||||
},
|
||||
];
|
|
@ -3,6 +3,7 @@ export const MINIMUM_SEARCH_LENGTH = 3;
|
|||
export const TYPE_BOARD = 'Board';
|
||||
export const TYPE_CI_RUNNER = 'Ci::Runner';
|
||||
export const TYPE_CRM_CONTACT = 'CustomerRelations::Contact';
|
||||
export const TYPE_CRM_ORGANIZATION = 'CustomerRelations::Organization';
|
||||
export const TYPE_DISCUSSION = 'Discussion';
|
||||
export const TYPE_EPIC = 'Epic';
|
||||
export const TYPE_EPIC_BOARD = 'Boards::EpicBoard';
|
||||
|
|
|
@ -9,7 +9,7 @@ const LINK_TAG_PATTERN = '[{text}](url)';
|
|||
// a bullet point character (*+-) and an optional checkbox ([ ] [x])
|
||||
// OR a number with a . after it and an optional checkbox ([ ] [x])
|
||||
// followed by one or more whitespace characters
|
||||
const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isUl>[*+-])|(?<isOl>\d+\.))( \[([x ])\])?\s)(?<content>.)?/;
|
||||
const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isUl>[*+-])|(?<isOl>\d+\.))( \[([xX ])\])?\s)(?<content>.)?/;
|
||||
|
||||
function selectedText(text, textarea) {
|
||||
return text.substring(textarea.selectionStart, textarea.selectionEnd);
|
||||
|
@ -391,6 +391,8 @@ function handleContinueList(e, textArea) {
|
|||
itemToInsert = `${indent}${leader}`;
|
||||
}
|
||||
|
||||
itemToInsert = itemToInsert.replace(/\[x\]/i, '[ ]');
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
updateText({
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import initCrmContactsApp from '~/crm/contacts_bundle';
|
||||
import initCrmContactsApp from '~/crm/contacts/bundle';
|
||||
|
||||
initCrmContactsApp();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import initCrmOrganizationsApp from '~/crm/organizations_bundle';
|
||||
import initCrmOrganizationsApp from '~/crm/organizations/bundle';
|
||||
|
||||
initCrmOrganizationsApp();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlSkeletonLoading } from '@gitlab/ui';
|
||||
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
|
||||
import { createAlert } from '~/flash';
|
||||
import runnerJobsQuery from '../graphql/details/runner_jobs.query.graphql';
|
||||
import { I18N_FETCH_ERROR, I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '../constants';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlSkeletonLoading } from '@gitlab/ui';
|
||||
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
|
||||
import { sprintf, formatNumber } from '~/locale';
|
||||
import { createAlert } from '~/flash';
|
||||
import runnerProjectsQuery from '../graphql/details/runner_projects.query.graphql';
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { GlButton, GlCard, GlIcon, GlLink } from '@gitlab/ui';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
|
||||
import { REPORT_TYPE_SAST_IAC } from '~/vue_shared/security_reports/constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -61,6 +62,12 @@ export default {
|
|||
const { name, description, configurationText } = this.feature.secondary ?? {};
|
||||
return Boolean(name && description && configurationText);
|
||||
},
|
||||
// This condition is a temporary hack to not display any wrong information
|
||||
// until this BE Bug is fixed: https://gitlab.com/gitlab-org/gitlab/-/issues/350307.
|
||||
// More Information: https://gitlab.com/gitlab-org/gitlab/-/issues/350307#note_825447417
|
||||
isNotSastIACTemporaryHack() {
|
||||
return this.feature.type !== REPORT_TYPE_SAST_IAC;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onError(message) {
|
||||
|
@ -85,6 +92,7 @@ export default {
|
|||
<h3 class="gl-font-lg gl-m-0 gl-mr-3">{{ feature.name }}</h3>
|
||||
|
||||
<div
|
||||
v-if="isNotSastIACTemporaryHack"
|
||||
:class="statusClasses"
|
||||
data-testid="feature-status"
|
||||
:data-qa-selector="`${feature.type}_status`"
|
||||
|
@ -109,7 +117,7 @@ export default {
|
|||
<gl-link :href="feature.helpPath">{{ $options.i18n.learnMore }}</gl-link>
|
||||
</p>
|
||||
|
||||
<template v-if="available">
|
||||
<template v-if="available && isNotSastIACTemporaryHack">
|
||||
<gl-button
|
||||
v-if="feature.configurationPath"
|
||||
:href="feature.configurationPath"
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
<script>
|
||||
import { GlAlert, GlKeysetPagination, GlSkeletonLoading, GlPagination } from '@gitlab/ui';
|
||||
import {
|
||||
GlAlert,
|
||||
GlKeysetPagination,
|
||||
GlDeprecatedSkeletonLoading as GlSkeletonLoading,
|
||||
GlPagination,
|
||||
} from '@gitlab/ui';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
|
||||
|
|
|
@ -10,6 +10,10 @@ class Groups::Crm::OrganizationsController < Groups::ApplicationController
|
|||
render action: "index"
|
||||
end
|
||||
|
||||
def edit
|
||||
render action: "index"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize_read_crm_organization!
|
||||
|
|
|
@ -81,7 +81,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
|
|||
def branch_to
|
||||
@target_project = selected_target_project
|
||||
|
||||
if @target_project && params[:ref].present?
|
||||
if @target_project && params[:ref].present? && Ability.allowed?(current_user, :create_merge_request_in, @target_project)
|
||||
@ref = params[:ref]
|
||||
@commit = @target_project.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref)
|
||||
end
|
||||
|
|
|
@ -824,6 +824,8 @@ module Ci
|
|||
variables.append(key: 'CI_OPEN_MERGE_REQUESTS', value: open_merge_requests_refs.join(','))
|
||||
end
|
||||
|
||||
variables.append(key: 'CI_GITLAB_FIPS_MODE', value: 'true') if Gitlab::FIPS.enabled?
|
||||
|
||||
variables.append(key: 'CI_KUBERNETES_ACTIVE', value: 'true') if has_kubernetes_active?
|
||||
variables.append(key: 'CI_DEPLOY_FREEZE', value: 'true') if freeze_period?
|
||||
|
||||
|
|
|
@ -65,6 +65,8 @@ module Ci
|
|||
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
|
||||
MINUTES_COST_FACTOR_FIELDS = %i[public_projects_minutes_cost_factor private_projects_minutes_cost_factor].freeze
|
||||
|
||||
TAG_LIST_MAX_LENGTH = 50
|
||||
|
||||
has_many :builds
|
||||
has_many :runner_projects, inverse_of: :runner, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :projects, through: :runner_projects, disable_joins: true
|
||||
|
@ -520,6 +522,11 @@ module Ci
|
|||
errors.add(:tags_list,
|
||||
'can not be empty when runner is not allowed to pick untagged jobs')
|
||||
end
|
||||
|
||||
if tag_list_changed? && tag_list.count > TAG_LIST_MAX_LENGTH
|
||||
errors.add(:tags_list,
|
||||
"Too many tags specified. Please limit the number of tags to #{TAG_LIST_MAX_LENGTH}")
|
||||
end
|
||||
end
|
||||
|
||||
def no_projects
|
||||
|
|
|
@ -59,12 +59,9 @@ module Integrations
|
|||
def execute(data)
|
||||
return unless supported_events.include?(data[:object_kind])
|
||||
|
||||
# check the branch restriction is poplulated and branch is not included
|
||||
branch = Gitlab::Git.ref_name(data[:ref])
|
||||
branch_restriction = restrict_to_branch.to_s
|
||||
if branch_restriction.present? && branch_restriction.index(branch).nil?
|
||||
return
|
||||
end
|
||||
|
||||
return unless branch_allowed?(branch)
|
||||
|
||||
user = data[:user_name]
|
||||
project_name = project.full_name
|
||||
|
@ -103,5 +100,13 @@ module Integrations
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def branch_allowed?(branch_name)
|
||||
return true if restrict_to_branch.blank?
|
||||
|
||||
restrict_to_branch.to_s.gsub(/\s+/, '').split(',').include?(branch_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,10 +9,20 @@ module Releases
|
|||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/218753
|
||||
# Regex modified to prevent catastrophic backtracking
|
||||
FILEPATH_REGEX = %r{\A\/[^\/](?!.*\/\/.*)[\-\.\w\/]+[\da-zA-Z]+\z}.freeze
|
||||
FILEPATH_MAX_LENGTH = 128
|
||||
|
||||
validates :url, presence: true, addressable_url: { schemes: %w(http https ftp) }, uniqueness: { scope: :release }
|
||||
validates :name, presence: true, uniqueness: { scope: :release }
|
||||
validates :filepath, uniqueness: { scope: :release }, format: { with: FILEPATH_REGEX }, allow_blank: true, length: { maximum: 128 }
|
||||
validates :filepath, uniqueness: { scope: :release }, allow_blank: true
|
||||
validate :filepath_format_valid?
|
||||
|
||||
# we use a custom validator here to prevent running the regex if the string is too long
|
||||
# see https://gitlab.com/gitlab-org/gitlab/-/issues/273771
|
||||
def filepath_format_valid?
|
||||
return if filepath.nil? # valid use case
|
||||
return errors.add(:filepath, "is too long (maximum is #{FILEPATH_MAX_LENGTH} characters)") if filepath.length > FILEPATH_MAX_LENGTH
|
||||
return errors.add(:filepath, 'is in an invalid format') unless FILEPATH_REGEX.match? filepath
|
||||
end
|
||||
|
||||
scope :sorted, -> { order(created_at: :desc) }
|
||||
|
||||
|
|
|
@ -46,11 +46,11 @@ class SshHostKey
|
|||
.select(&:valid?)
|
||||
end
|
||||
|
||||
attr_reader :project, :url, :compare_host_keys
|
||||
attr_reader :project, :url, :ip, :compare_host_keys
|
||||
|
||||
def initialize(project:, url:, compare_host_keys: nil)
|
||||
@project = project
|
||||
@url = normalize_url(url)
|
||||
@url, @ip = normalize_url(url)
|
||||
@compare_host_keys = compare_host_keys
|
||||
end
|
||||
|
||||
|
@ -90,9 +90,11 @@ class SshHostKey
|
|||
end
|
||||
|
||||
def calculate_reactive_cache
|
||||
input = [ip, url.hostname].compact.join(' ')
|
||||
|
||||
known_hosts, errors, status =
|
||||
Open3.popen3({}, *%W[ssh-keyscan -T 5 -p #{url.port} -f-]) do |stdin, stdout, stderr, wait_thr|
|
||||
stdin.puts(url.host)
|
||||
stdin.puts(input)
|
||||
stdin.close
|
||||
|
||||
[
|
||||
|
@ -127,11 +129,31 @@ class SshHostKey
|
|||
end
|
||||
|
||||
def normalize_url(url)
|
||||
full_url = ::Addressable::URI.parse(url)
|
||||
raise ArgumentError, "Invalid URL" unless full_url&.scheme == 'ssh'
|
||||
url, real_hostname = Gitlab::UrlBlocker.validate!(
|
||||
url,
|
||||
schemes: %w[ssh],
|
||||
allow_localhost: allow_local_requests?,
|
||||
allow_local_network: allow_local_requests?,
|
||||
dns_rebind_protection: Gitlab::CurrentSettings.dns_rebinding_protection_enabled?
|
||||
)
|
||||
|
||||
Addressable::URI.parse("ssh://#{full_url.host}:#{full_url.inferred_port}")
|
||||
rescue Addressable::URI::InvalidURIError
|
||||
# When DNS rebinding protection is required, the hostname is replaced by the
|
||||
# resolved IP. However, `url` is used in `id`, so we can't change it. Track
|
||||
# the resolved IP separately instead.
|
||||
if real_hostname
|
||||
ip = url.hostname
|
||||
url.hostname = real_hostname
|
||||
end
|
||||
|
||||
# Ensure ssh://foo and ssh://foo:22 share the same cache
|
||||
url.port = url.inferred_port
|
||||
|
||||
[url, ip]
|
||||
rescue Gitlab::UrlBlocker::BlockedUrlError
|
||||
raise ArgumentError, "Invalid URL"
|
||||
end
|
||||
|
||||
def allow_local_requests?
|
||||
Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -879,6 +879,23 @@ class User < ApplicationRecord
|
|||
reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
|
||||
end
|
||||
|
||||
# See https://gitlab.com/gitlab-org/security/gitlab/-/issues/638
|
||||
DISALLOWED_PASSWORDS = %w[123qweQWE!@#000000000].freeze
|
||||
|
||||
# Overwrites valid_password? from Devise::Models::DatabaseAuthenticatable
|
||||
# In constant-time, check both that the password isn't on a denylist AND
|
||||
# that the password is the user's password
|
||||
def valid_password?(password)
|
||||
password_allowed = true
|
||||
DISALLOWED_PASSWORDS.each do |disallowed_password|
|
||||
password_allowed = false if Devise.secure_compare(password, disallowed_password)
|
||||
end
|
||||
|
||||
original_result = super
|
||||
|
||||
password_allowed && original_result
|
||||
end
|
||||
|
||||
def remember_me!
|
||||
super if ::Gitlab::Database.read_write?
|
||||
end
|
||||
|
|
|
@ -240,7 +240,6 @@ class ProjectPolicy < BasePolicy
|
|||
|
||||
rule { can?(:guest_access) }.policy do
|
||||
enable :read_project
|
||||
enable :create_merge_request_in
|
||||
enable :read_issue_board
|
||||
enable :read_issue_board_list
|
||||
enable :read_wiki
|
||||
|
@ -497,7 +496,7 @@ class ProjectPolicy < BasePolicy
|
|||
prevent(*create_read_update_admin_destroy(:issue_board_list))
|
||||
end
|
||||
|
||||
rule { merge_requests_disabled | repository_disabled }.policy do
|
||||
rule { merge_requests_disabled | repository_disabled | ~can?(:download_code) }.policy do
|
||||
prevent :create_merge_request_in
|
||||
prevent :create_merge_request_from
|
||||
prevent(*create_read_update_admin_destroy(:merge_request))
|
||||
|
@ -600,13 +599,14 @@ class ProjectPolicy < BasePolicy
|
|||
enable :read_cycle_analytics
|
||||
enable :read_pages_content
|
||||
enable :read_analytics
|
||||
enable :read_ci_cd_analytics
|
||||
enable :read_insights
|
||||
|
||||
# NOTE: may be overridden by IssuePolicy
|
||||
enable :read_issue
|
||||
end
|
||||
|
||||
rule { can?(:public_access) & public_builds }.enable :read_ci_cd_analytics
|
||||
|
||||
rule { public_builds }.policy do
|
||||
enable :read_build
|
||||
end
|
||||
|
@ -664,6 +664,10 @@ class ProjectPolicy < BasePolicy
|
|||
enable :read_security_configuration
|
||||
end
|
||||
|
||||
rule { can?(:guest_access) & can?(:read_commit_status) }.policy do
|
||||
enable :create_merge_request_in
|
||||
end
|
||||
|
||||
# Design abilities could also be prevented in the issue policy.
|
||||
rule { design_management_disabled }.policy do
|
||||
prevent :read_design
|
||||
|
|
|
@ -77,7 +77,10 @@ module QuickActions
|
|||
# want to also handle bare usernames. The ReferenceExtractor also has
|
||||
# different behaviour, and will return all group members for groups named
|
||||
# using a user-style reference, which is not in scope here.
|
||||
#
|
||||
# nb: underscores may be passed in escaped to protect them from markdown rendering
|
||||
args = params.split(/\s|,/).select(&:present?).uniq - ['and']
|
||||
args.map! { _1.gsub(/\\_/, '_') }
|
||||
usernames = (args - ['me']).map { _1.delete_prefix('@') }
|
||||
found = User.by_username(usernames).to_a.select { can?(:read_user, _1) }
|
||||
found_names = found.map(&:username).to_set
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
- breadcrumb_title _('Customer Relations Contacts')
|
||||
- page_title _('Customer Relations Contacts')
|
||||
- breadcrumb_title _('Customer relations contacts')
|
||||
- page_title _('Customer relations contacts')
|
||||
- @content_wrapper_class = "gl-relative"
|
||||
|
||||
= content_for :after_content do
|
||||
#js-crm-form-portal
|
||||
|
||||
#js-crm-contacts-app{ data: { group_full_path: @group.full_path, group_issues_path: issues_group_path(@group), group_id: @group.id, can_admin_crm_contact: can?(current_user, :admin_crm_contact, @group).to_s, base_path: group_crm_contacts_path(@group) } }
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
- breadcrumb_title _('Customer Relations Organizations')
|
||||
- page_title _('Customer Relations Organizations')
|
||||
- breadcrumb_title _('Customer relations organizations')
|
||||
- page_title _('Customer relations organizations')
|
||||
- @content_wrapper_class = "gl-relative"
|
||||
|
||||
= content_for :after_content do
|
||||
#js-crm-form-portal
|
||||
|
||||
#js-crm-organizations-app{ data: { base_path: group_crm_organizations_path(@group), can_admin_crm_organization: can?(current_user, :admin_crm_organization, @group).to_s, group_full_path: @group.full_path, group_id: @group.id, group_issues_path: issues_group_path(@group) } }
|
||||
|
|
21
config/initializers/rdoc_segfault_patch.rb
Normal file
21
config/initializers/rdoc_segfault_patch.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Monkey patch of RDoc to prevent Ruby segfault due to
|
||||
# stack buffer overflow Ruby bug -
|
||||
# https://bugs.ruby-lang.org/issues/16376
|
||||
#
|
||||
# Safe to remove once GitLab upgrades to Ruby 3.0
|
||||
# or once the fix is backported to 2.7.x and
|
||||
# GitLab upgrades.
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/351179
|
||||
class RDoc::Markup::ToHtml
|
||||
def parseable?(_)
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
class RDoc::Markup::Verbatim
|
||||
def ruby?
|
||||
false
|
||||
end
|
||||
end
|
|
@ -135,7 +135,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
|
|||
|
||||
namespace :crm do
|
||||
resources :contacts, only: [:index, :new, :edit]
|
||||
resources :organizations, only: [:index, :new]
|
||||
resources :organizations, only: [:index, :new, :edit]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ module Db
|
|||
name: FFaker::Name.name,
|
||||
email: FFaker::Internet.email,
|
||||
confirmed_at: DateTime.now,
|
||||
password: Gitlab::Password.test_default
|
||||
password: '12345678'
|
||||
)
|
||||
|
||||
::AbuseReport.create(reporter: ::User.take, user: reported_user, message: 'User sends spam')
|
||||
|
|
|
@ -58,6 +58,7 @@ There are also a number of [variables you can use to configure runner behavior](
|
|||
| `CI_ENVIRONMENT_URL` | 9.3 | all | The URL of the environment for this job. Available if [`environment:url`](../yaml/index.md#environmenturl) is set. |
|
||||
| `CI_ENVIRONMENT_ACTION` | 13.11 | all | The action annotation specified for this job's environment. Available if [`environment:action`](../yaml/index.md#environmentaction) is set. Can be `start`, `prepare`, or `stop`. |
|
||||
| `CI_ENVIRONMENT_TIER` | 14.0 | all | The [deployment tier of the environment](../environments/index.md#deployment-tier-of-environments) for this job. |
|
||||
| `CI_GITLAB_FIPS_MODE` | 14.10 | all | The configuration setting for whether FIPS mode is enabled in the GitLab instance. |
|
||||
| `CI_HAS_OPEN_REQUIREMENTS` | 13.1 | all | Only available if the pipeline's project has an open [requirement](../../user/project/requirements/index.md). `true` when available. |
|
||||
| `CI_JOB_ID` | 9.0 | all | The internal ID of the job, unique across all jobs in the GitLab instance. |
|
||||
| `CI_JOB_IMAGE` | 12.9 | 12.9 | The name of the Docker image running the job. |
|
||||
|
|
BIN
doc/user/crm/crm_contacts_v14_10.png
Normal file
BIN
doc/user/crm/crm_contacts_v14_10.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
Before Width: | Height: | Size: 19 KiB |
BIN
doc/user/crm/crm_organizations_v14_10.png
Normal file
BIN
doc/user/crm/crm_organizations_v14_10.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.1 KiB |
|
@ -49,7 +49,7 @@ To view a group's contacts:
|
|||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Customer relations > Contacts**.
|
||||
|
||||
![Contacts list](crm_contacts_v14_6.png)
|
||||
![Contacts list](crm_contacts_v14_10.png)
|
||||
|
||||
### Create a contact
|
||||
|
||||
|
@ -86,7 +86,7 @@ To view a group's organizations:
|
|||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Customer relations > Organizations**.
|
||||
|
||||
![Organizations list](crm_organizations_v14_6.png)
|
||||
![Organizations list](crm_organizations_v14_10.png)
|
||||
|
||||
### Create an organization
|
||||
|
||||
|
|
|
@ -6,8 +6,10 @@ require "asciidoctor/extensions/asciidoctor_kroki/extension"
|
|||
module Banzai
|
||||
module Filter
|
||||
# HTML that replaces all diagrams supported by Kroki with the corresponding img tags.
|
||||
#
|
||||
# If the source content is large then the hidden attribute is added to the img tag.
|
||||
class KrokiFilter < HTML::Pipeline::Filter
|
||||
MAX_CHARACTER_LIMIT = 2000
|
||||
|
||||
def call
|
||||
return doc unless settings.kroki_enabled
|
||||
|
||||
|
@ -21,10 +23,16 @@ module Banzai
|
|||
diagram_format = "svg"
|
||||
doc.xpath(xpath).each do |node|
|
||||
diagram_type = node.parent['lang']
|
||||
img_tag = Nokogiri::HTML::DocumentFragment.parse(%(<img src="#{create_image_src(diagram_type, diagram_format, node.content)}"/>))
|
||||
diagram_src = node.content
|
||||
image_src = create_image_src(diagram_type, diagram_format, diagram_src)
|
||||
img_tag = Nokogiri::HTML::DocumentFragment.parse(%(<img src="#{image_src}" />))
|
||||
img_tag = img_tag.children.first
|
||||
|
||||
unless img_tag.nil?
|
||||
lazy_load = diagram_src.length > MAX_CHARACTER_LIMIT
|
||||
img_tag.set_attribute('hidden', '') if lazy_load
|
||||
img_tag.set_attribute('class', 'js-render-kroki')
|
||||
|
||||
img_tag.set_attribute('data-diagram', node.parent['lang'])
|
||||
img_tag.set_attribute('data-diagram-src', "data:text/plain;base64,#{Base64.strict_encode64(node.content)}")
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ module Banzai
|
|||
retry
|
||||
end
|
||||
|
||||
sourcepos_attr = sourcepos ? "data-sourcepos=\"#{sourcepos}\"" : ''
|
||||
sourcepos_attr = sourcepos ? "data-sourcepos=\"#{escape_once(sourcepos)}\"" : ''
|
||||
|
||||
highlighted = %(<div class="gl-relative markdown-code-block js-markdown-code"><pre #{sourcepos_attr} class="#{css_classes}"
|
||||
lang="#{language}"
|
||||
|
|
|
@ -12,10 +12,10 @@ module Banzai
|
|||
def self.filters
|
||||
@filters ||= FilterArray[
|
||||
Filter::PlantumlFilter,
|
||||
Filter::KrokiFilter,
|
||||
# Must always be before the SanitizationFilter to prevent XSS attacks
|
||||
Filter::SpacedLinkFilter,
|
||||
Filter::SanitizationFilter,
|
||||
Filter::KrokiFilter,
|
||||
Filter::AssetProxyFilter,
|
||||
Filter::SyntaxHighlightFilter,
|
||||
Filter::MathFilter,
|
||||
|
|
|
@ -65,16 +65,15 @@ module Banzai
|
|||
#
|
||||
def redacted_node_content(node)
|
||||
original_content = node.attr('data-original')
|
||||
link_reference = node.attr('data-link-reference')
|
||||
original_content = CGI.escape_html(original_content) if original_content
|
||||
|
||||
# Build the raw <a> tag just with a link as href and content if
|
||||
# it's originally a link pattern. We shouldn't return a plain text href.
|
||||
original_link =
|
||||
if link_reference == 'true'
|
||||
if node.attr('data-link-reference') == 'true'
|
||||
href = node.attr('href')
|
||||
content = original_content
|
||||
|
||||
%(<a href="#{href}">#{content}</a>)
|
||||
%(<a href="#{href}">#{original_content}</a>)
|
||||
end
|
||||
|
||||
# The reference should be replaced by the original link's content,
|
||||
|
|
|
@ -239,8 +239,8 @@ module Gitlab
|
|||
name: name.strip.presence || valid_username,
|
||||
username: valid_username,
|
||||
email: email,
|
||||
password: Gitlab::Password.test_default(21),
|
||||
password_confirmation: Gitlab::Password.test_default(21),
|
||||
password: auth_hash.password,
|
||||
password_confirmation: auth_hash.password,
|
||||
password_automatically_set: true
|
||||
}
|
||||
end
|
||||
|
|
10
lib/gitlab/ci/config/external/context.rb
vendored
10
lib/gitlab/ci/config/external/context.rb
vendored
|
@ -70,6 +70,16 @@ module Gitlab
|
|||
}
|
||||
end
|
||||
|
||||
def mask_variables_from(location)
|
||||
variables.reduce(location.dup) do |loc, variable|
|
||||
if variable[:masked]
|
||||
Gitlab::Ci::MaskSecret.mask!(loc, variable[:value])
|
||||
else
|
||||
loc
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_writer :expandset, :execution_deadline, :logger
|
||||
|
|
|
@ -37,7 +37,7 @@ module Gitlab
|
|||
def validate_content!
|
||||
return unless ensure_preconditions_satisfied!
|
||||
|
||||
errors.push("File `#{location}` is empty!") unless content.present?
|
||||
errors.push("File `#{masked_location}` is empty!") unless content.present?
|
||||
end
|
||||
|
||||
def ensure_preconditions_satisfied!
|
||||
|
|
14
lib/gitlab/ci/config/external/file/base.rb
vendored
14
lib/gitlab/ci/config/external/file/base.rb
vendored
|
@ -79,21 +79,21 @@ module Gitlab
|
|||
|
||||
def validate_location!
|
||||
if invalid_location_type?
|
||||
errors.push("Included file `#{location}` needs to be a string")
|
||||
errors.push("Included file `#{masked_location}` needs to be a string")
|
||||
elsif invalid_extension?
|
||||
errors.push("Included file `#{location}` does not have YAML extension!")
|
||||
errors.push("Included file `#{masked_location}` does not have YAML extension!")
|
||||
end
|
||||
end
|
||||
|
||||
def validate_content!
|
||||
if content.blank?
|
||||
errors.push("Included file `#{location}` is empty or does not exist!")
|
||||
errors.push("Included file `#{masked_location}` is empty or does not exist!")
|
||||
end
|
||||
end
|
||||
|
||||
def validate_hash!
|
||||
if to_hash.blank?
|
||||
errors.push("Included file `#{location}` does not have valid YAML syntax!")
|
||||
errors.push("Included file `#{masked_location}` does not have valid YAML syntax!")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -104,6 +104,12 @@ module Gitlab
|
|||
def expand_context_attrs
|
||||
{}
|
||||
end
|
||||
|
||||
def masked_location
|
||||
strong_memoize(:masked_location) do
|
||||
context.mask_variables_from(location)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
6
lib/gitlab/ci/config/external/file/local.rb
vendored
6
lib/gitlab/ci/config/external/file/local.rb
vendored
|
@ -23,11 +23,11 @@ module Gitlab
|
|||
|
||||
def validate_content!
|
||||
if context.project&.repository.nil?
|
||||
errors.push("Local file `#{location}` does not have project!")
|
||||
errors.push("Local file `#{masked_location}` does not have project!")
|
||||
elsif content.nil?
|
||||
errors.push("Local file `#{location}` does not exist!")
|
||||
errors.push("Local file `#{masked_location}` does not exist!")
|
||||
elsif content.blank?
|
||||
errors.push("Local file `#{location}` is empty!")
|
||||
errors.push("Local file `#{masked_location}` is empty!")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -35,9 +35,9 @@ module Gitlab
|
|||
elsif sha.nil?
|
||||
errors.push("Project `#{project_name}` reference `#{ref_name}` does not exist!")
|
||||
elsif content.nil?
|
||||
errors.push("Project `#{project_name}` file `#{location}` does not exist!")
|
||||
errors.push("Project `#{project_name}` file `#{masked_location}` does not exist!")
|
||||
elsif content.blank?
|
||||
errors.push("Project `#{project_name}` file `#{location}` is empty!")
|
||||
errors.push("Project `#{project_name}` file `#{masked_location}` is empty!")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
10
lib/gitlab/ci/config/external/file/remote.rb
vendored
10
lib/gitlab/ci/config/external/file/remote.rb
vendored
|
@ -24,7 +24,7 @@ module Gitlab
|
|||
super
|
||||
|
||||
unless ::Gitlab::UrlSanitizer.valid?(location)
|
||||
errors.push("Remote file `#{location}` does not have a valid address!")
|
||||
errors.push("Remote file `#{masked_location}` does not have a valid address!")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -32,17 +32,17 @@ module Gitlab
|
|||
begin
|
||||
response = Gitlab::HTTP.get(location)
|
||||
rescue SocketError
|
||||
errors.push("Remote file `#{location}` could not be fetched because of a socket error!")
|
||||
errors.push("Remote file `#{masked_location}` could not be fetched because of a socket error!")
|
||||
rescue Timeout::Error
|
||||
errors.push("Remote file `#{location}` could not be fetched because of a timeout error!")
|
||||
errors.push("Remote file `#{masked_location}` could not be fetched because of a timeout error!")
|
||||
rescue Gitlab::HTTP::Error
|
||||
errors.push("Remote file `#{location}` could not be fetched because of HTTP error!")
|
||||
errors.push("Remote file `#{masked_location}` could not be fetched because of HTTP error!")
|
||||
rescue Gitlab::HTTP::BlockedUrlError => e
|
||||
errors.push("Remote file could not be fetched because #{e}!")
|
||||
end
|
||||
|
||||
if response&.code.to_i >= 400
|
||||
errors.push("Remote file `#{location}` could not be fetched because of HTTP code `#{response.code}` error!")
|
||||
errors.push("Remote file `#{masked_location}` could not be fetched because of HTTP code `#{response.code}` error!")
|
||||
end
|
||||
|
||||
response.body if errors.none?
|
||||
|
|
|
@ -26,7 +26,7 @@ module Gitlab
|
|||
super
|
||||
|
||||
unless template_name_valid?
|
||||
errors.push("Template file `#{location}` is not a valid location!")
|
||||
errors.push("Template file `#{masked_location}` is not a valid location!")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
6
lib/gitlab/ci/config/external/mapper.rb
vendored
6
lib/gitlab/ci/config/external/mapper.rb
vendored
|
@ -143,7 +143,7 @@ module Gitlab
|
|||
file_class.new(location, context)
|
||||
end.select(&:matching?)
|
||||
|
||||
raise AmbigiousSpecificationError, "Include `#{location.to_json}` needs to match exactly one accessor!" unless matching.one?
|
||||
raise AmbigiousSpecificationError, "Include `#{masked_location(location.to_json)}` needs to match exactly one accessor!" unless matching.one?
|
||||
|
||||
matching.first
|
||||
end
|
||||
|
@ -182,6 +182,10 @@ module Gitlab
|
|||
def expand(data)
|
||||
ExpandVariables.expand(data, -> { context.variables_hash })
|
||||
end
|
||||
|
||||
def masked_location(location)
|
||||
context.mask_variables_from(location)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,10 +4,9 @@ module Gitlab
|
|||
module Database
|
||||
module BackgroundMigration
|
||||
class BatchedMigrationWrapper
|
||||
extend Gitlab::Utils::StrongMemoize
|
||||
|
||||
def initialize(connection: ApplicationRecord.connection)
|
||||
def initialize(connection: ApplicationRecord.connection, metrics: PrometheusMetrics.new)
|
||||
@connection = connection
|
||||
@metrics = metrics
|
||||
end
|
||||
|
||||
# Wraps the execution of a batched_background_migration.
|
||||
|
@ -28,12 +27,12 @@ module Gitlab
|
|||
|
||||
raise
|
||||
ensure
|
||||
track_prometheus_metrics(batch_tracking_record)
|
||||
metrics.track(batch_tracking_record)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :connection
|
||||
attr_reader :connection, :metrics
|
||||
|
||||
def start_tracking_execution(tracking_record)
|
||||
tracking_record.run!
|
||||
|
@ -63,80 +62,6 @@ module Gitlab
|
|||
job_class.new
|
||||
end
|
||||
end
|
||||
|
||||
def track_prometheus_metrics(tracking_record)
|
||||
migration = tracking_record.batched_migration
|
||||
base_labels = migration.prometheus_labels
|
||||
|
||||
metric_for(:gauge_batch_size).set(base_labels, tracking_record.batch_size)
|
||||
metric_for(:gauge_sub_batch_size).set(base_labels, tracking_record.sub_batch_size)
|
||||
metric_for(:gauge_interval).set(base_labels, tracking_record.batched_migration.interval)
|
||||
metric_for(:gauge_job_duration).set(base_labels, (tracking_record.finished_at - tracking_record.started_at).to_i)
|
||||
metric_for(:counter_updated_tuples).increment(base_labels, tracking_record.batch_size)
|
||||
metric_for(:gauge_migrated_tuples).set(base_labels, tracking_record.batched_migration.migrated_tuple_count)
|
||||
metric_for(:gauge_total_tuple_count).set(base_labels, tracking_record.batched_migration.total_tuple_count)
|
||||
metric_for(:gauge_last_update_time).set(base_labels, Time.current.to_i)
|
||||
|
||||
if metrics = tracking_record.metrics
|
||||
metrics['timings']&.each do |key, timings|
|
||||
summary = metric_for(:histogram_timings)
|
||||
labels = base_labels.merge(operation: key)
|
||||
|
||||
timings.each do |timing|
|
||||
summary.observe(labels, timing)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def metric_for(name)
|
||||
self.class.metrics[name]
|
||||
end
|
||||
|
||||
def self.metrics
|
||||
strong_memoize(:metrics) do
|
||||
{
|
||||
gauge_batch_size: Gitlab::Metrics.gauge(
|
||||
:batched_migration_job_batch_size,
|
||||
'Batch size for a batched migration job'
|
||||
),
|
||||
gauge_sub_batch_size: Gitlab::Metrics.gauge(
|
||||
:batched_migration_job_sub_batch_size,
|
||||
'Sub-batch size for a batched migration job'
|
||||
),
|
||||
gauge_interval: Gitlab::Metrics.gauge(
|
||||
:batched_migration_job_interval_seconds,
|
||||
'Interval for a batched migration job'
|
||||
),
|
||||
gauge_job_duration: Gitlab::Metrics.gauge(
|
||||
:batched_migration_job_duration_seconds,
|
||||
'Duration for a batched migration job'
|
||||
),
|
||||
counter_updated_tuples: Gitlab::Metrics.counter(
|
||||
:batched_migration_job_updated_tuples_total,
|
||||
'Number of tuples updated by batched migration job'
|
||||
),
|
||||
gauge_migrated_tuples: Gitlab::Metrics.gauge(
|
||||
:batched_migration_migrated_tuples_total,
|
||||
'Total number of tuples migrated by a batched migration'
|
||||
),
|
||||
histogram_timings: Gitlab::Metrics.histogram(
|
||||
:batched_migration_job_query_duration_seconds,
|
||||
'Query timings for a batched migration job',
|
||||
{},
|
||||
[0.1, 0.25, 0.5, 1, 5].freeze
|
||||
),
|
||||
gauge_total_tuple_count: Gitlab::Metrics.gauge(
|
||||
:batched_migration_total_tuple_count,
|
||||
'Total tuple count the migration needs to touch'
|
||||
),
|
||||
gauge_last_update_time: Gitlab::Metrics.gauge(
|
||||
:batched_migration_last_update_time_seconds,
|
||||
'Unix epoch time in seconds'
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module BackgroundMigration
|
||||
class PrometheusMetrics
|
||||
extend Gitlab::Utils::StrongMemoize
|
||||
|
||||
QUERY_TIMING_BUCKETS = [0.1, 0.25, 0.5, 1, 5].freeze
|
||||
|
||||
def track(job_record)
|
||||
migration_record = job_record.batched_migration
|
||||
base_labels = migration_record.prometheus_labels
|
||||
|
||||
metric_for(:gauge_batch_size).set(base_labels, job_record.batch_size)
|
||||
metric_for(:gauge_sub_batch_size).set(base_labels, job_record.sub_batch_size)
|
||||
metric_for(:gauge_interval).set(base_labels, job_record.batched_migration.interval)
|
||||
metric_for(:gauge_job_duration).set(base_labels, (job_record.finished_at - job_record.started_at).to_i)
|
||||
metric_for(:counter_updated_tuples).increment(base_labels, job_record.batch_size)
|
||||
metric_for(:gauge_migrated_tuples).set(base_labels, migration_record.migrated_tuple_count)
|
||||
metric_for(:gauge_total_tuple_count).set(base_labels, migration_record.total_tuple_count)
|
||||
metric_for(:gauge_last_update_time).set(base_labels, Time.current.to_i)
|
||||
|
||||
track_timing_metrics(base_labels, job_record.metrics)
|
||||
end
|
||||
|
||||
def self.metrics
|
||||
strong_memoize(:metrics) do
|
||||
{
|
||||
gauge_batch_size: Gitlab::Metrics.gauge(
|
||||
:batched_migration_job_batch_size,
|
||||
'Batch size for a batched migration job'
|
||||
),
|
||||
gauge_sub_batch_size: Gitlab::Metrics.gauge(
|
||||
:batched_migration_job_sub_batch_size,
|
||||
'Sub-batch size for a batched migration job'
|
||||
),
|
||||
gauge_interval: Gitlab::Metrics.gauge(
|
||||
:batched_migration_job_interval_seconds,
|
||||
'Interval for a batched migration job'
|
||||
),
|
||||
gauge_job_duration: Gitlab::Metrics.gauge(
|
||||
:batched_migration_job_duration_seconds,
|
||||
'Duration for a batched migration job'
|
||||
),
|
||||
counter_updated_tuples: Gitlab::Metrics.counter(
|
||||
:batched_migration_job_updated_tuples_total,
|
||||
'Number of tuples updated by batched migration job'
|
||||
),
|
||||
gauge_migrated_tuples: Gitlab::Metrics.gauge(
|
||||
:batched_migration_migrated_tuples_total,
|
||||
'Total number of tuples migrated by a batched migration'
|
||||
),
|
||||
histogram_timings: Gitlab::Metrics.histogram(
|
||||
:batched_migration_job_query_duration_seconds,
|
||||
'Query timings for a batched migration job',
|
||||
{},
|
||||
QUERY_TIMING_BUCKETS
|
||||
),
|
||||
gauge_total_tuple_count: Gitlab::Metrics.gauge(
|
||||
:batched_migration_total_tuple_count,
|
||||
'Total tuple count the migration needs to touch'
|
||||
),
|
||||
gauge_last_update_time: Gitlab::Metrics.gauge(
|
||||
:batched_migration_last_update_time_seconds,
|
||||
'Unix epoch time in seconds'
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def track_timing_metrics(base_labels, metrics)
|
||||
return unless metrics && metrics['timings']
|
||||
|
||||
metrics['timings'].each do |key, timings|
|
||||
summary = metric_for(:histogram_timings)
|
||||
labels = base_labels.merge(operation: key)
|
||||
|
||||
timings.each do |timing|
|
||||
summary.observe(labels, timing)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def metric_for(name)
|
||||
self.class.metrics[name]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -19,7 +19,8 @@ module Gitlab
|
|||
PROCESSORS = [
|
||||
::Gitlab::ErrorTracking::Processor::SidekiqProcessor,
|
||||
::Gitlab::ErrorTracking::Processor::GrpcErrorProcessor,
|
||||
::Gitlab::ErrorTracking::Processor::ContextPayloadProcessor
|
||||
::Gitlab::ErrorTracking::Processor::ContextPayloadProcessor,
|
||||
::Gitlab::ErrorTracking::Processor::SanitizeErrorMessageProcessor
|
||||
].freeze
|
||||
|
||||
class << self
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module ErrorTracking
|
||||
module Processor
|
||||
module Concerns
|
||||
module ProcessesExceptions
|
||||
private
|
||||
|
||||
def extract_exceptions_from(event)
|
||||
exceptions = if event.is_a?(Raven::Event)
|
||||
event.instance_variable_get(:@interfaces)[:exception]&.values
|
||||
else
|
||||
event&.exception&.instance_variable_get(:@values)
|
||||
end
|
||||
|
||||
Array.wrap(exceptions)
|
||||
end
|
||||
|
||||
def set_exception_message(exception, message)
|
||||
if exception.respond_to?(:value=)
|
||||
exception.value = message
|
||||
else
|
||||
exception.instance_variable_set(:@value, message)
|
||||
end
|
||||
end
|
||||
|
||||
def valid_exception?(exception)
|
||||
case exception
|
||||
when Raven::SingleExceptionInterface, Sentry::SingleExceptionInterface
|
||||
exception&.value.present?
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,6 +4,8 @@ module Gitlab
|
|||
module ErrorTracking
|
||||
module Processor
|
||||
module GrpcErrorProcessor
|
||||
extend Gitlab::ErrorTracking::Processor::Concerns::ProcessesExceptions
|
||||
|
||||
DEBUG_ERROR_STRING_REGEX = RE2('(.*) debug_error_string:(.*)')
|
||||
|
||||
class << self
|
||||
|
@ -19,9 +21,6 @@ module Gitlab
|
|||
def process_first_exception_value(event)
|
||||
# Better in new version, will be event.exception.values
|
||||
exceptions = extract_exceptions_from(event)
|
||||
|
||||
return unless exceptions.is_a?(Array)
|
||||
|
||||
exception = exceptions.first
|
||||
|
||||
return unless valid_exception?(exception)
|
||||
|
@ -39,11 +38,7 @@ module Gitlab
|
|||
exceptions.each do |exception|
|
||||
next unless valid_exception?(exception)
|
||||
|
||||
if exception.respond_to?(:value=)
|
||||
exception.value = message
|
||||
else
|
||||
exception.instance_variable_set(:@value, message)
|
||||
end
|
||||
set_exception_message(exception, message)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -59,16 +54,6 @@ module Gitlab
|
|||
fingerprint[1] = message if message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def extract_exceptions_from(event)
|
||||
if event.is_a?(Raven::Event)
|
||||
event.instance_variable_get(:@interfaces)[:exception]&.values
|
||||
else
|
||||
event.exception&.instance_variable_get(:@values)
|
||||
end
|
||||
end
|
||||
|
||||
def custom_grpc_fingerprint?(fingerprint)
|
||||
fingerprint.is_a?(Array) && fingerprint.length == 2 && fingerprint[0].start_with?('GRPC::')
|
||||
end
|
||||
|
@ -82,15 +67,6 @@ module Gitlab
|
|||
|
||||
[match[1], match[2]]
|
||||
end
|
||||
|
||||
def valid_exception?(exception)
|
||||
case exception
|
||||
when Raven::SingleExceptionInterface, Sentry::SingleExceptionInterface
|
||||
exception&.value
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module ErrorTracking
|
||||
module Processor
|
||||
module SanitizeErrorMessageProcessor
|
||||
extend Gitlab::ErrorTracking::Processor::Concerns::ProcessesExceptions
|
||||
|
||||
class << self
|
||||
def call(event)
|
||||
exceptions = extract_exceptions_from(event)
|
||||
|
||||
exceptions.each do |exception|
|
||||
next unless valid_exception?(exception)
|
||||
|
||||
message = Gitlab::Sanitizers::ExceptionMessage.clean(exception.type, exception.value)
|
||||
|
||||
set_exception_message(exception, message)
|
||||
end
|
||||
|
||||
event
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@ module Gitlab
|
|||
# Use periods to flatten the fields.
|
||||
payload.merge!(
|
||||
'exception.class' => exception.class.name,
|
||||
'exception.message' => exception.message
|
||||
'exception.message' => sanitize_message(exception)
|
||||
)
|
||||
|
||||
if exception.backtrace
|
||||
|
@ -38,6 +38,10 @@ module Gitlab
|
|||
rescue PgQuery::ParseError
|
||||
sql
|
||||
end
|
||||
|
||||
def sanitize_message(exception)
|
||||
Gitlab::Sanitizers::ExceptionMessage.clean(exception.class.name, exception.message)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,9 +19,8 @@ module Gitlab
|
|||
@exported_members.inject(missing_keys_tracking_hash) do |hash, member|
|
||||
if member['user']
|
||||
old_user_id = member['user']['id']
|
||||
old_user_email = member.dig('user', 'public_email') || member.dig('user', 'email')
|
||||
existing_user = User.find_by(find_user_query(old_user_email)) if old_user_email
|
||||
hash[old_user_id] = existing_user.id if existing_user && add_team_member(member, existing_user)
|
||||
existing_user_id = existing_users_email_map[get_email(member)]
|
||||
hash[old_user_id] = existing_user_id if existing_user_id && add_team_member(member, existing_user_id)
|
||||
else
|
||||
add_team_member(member)
|
||||
end
|
||||
|
@ -72,11 +71,45 @@ module Gitlab
|
|||
member&.user == @user && member.access_level >= highest_access_level
|
||||
end
|
||||
|
||||
def add_team_member(member, existing_user = nil)
|
||||
return true if existing_user && @importable.members.exists?(user_id: existing_user.id)
|
||||
# Returns {email => user_id} hash where user_id is an ID at current instance
|
||||
def existing_users_email_map
|
||||
@existing_users_email_map ||= begin
|
||||
emails = @exported_members.map { |member| get_email(member) }
|
||||
|
||||
User.by_user_email(emails).pluck(:email, :id).to_h
|
||||
end
|
||||
end
|
||||
|
||||
# Returns {user_id => email} hash where user_id is an ID at source "old" instance
|
||||
def exported_members_email_map
|
||||
@exported_members_email_map ||= begin
|
||||
result = {}
|
||||
@exported_members.each do |member|
|
||||
email = get_email(member)
|
||||
|
||||
next unless email
|
||||
|
||||
result[member.dig('user', 'id')] = email
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
def get_email(member_data)
|
||||
return unless member_data['user']
|
||||
|
||||
member_data.dig('user', 'public_email') || member_data.dig('user', 'email')
|
||||
end
|
||||
|
||||
def add_team_member(member, existing_user_id = nil)
|
||||
return true if existing_user_id && @importable.members.exists?(user_id: existing_user_id)
|
||||
|
||||
member['user'] = existing_user
|
||||
member_hash = member_hash(member)
|
||||
if existing_user_id
|
||||
member_hash.delete('user')
|
||||
member_hash['user_id'] = existing_user_id
|
||||
end
|
||||
|
||||
member = relation_class.create(member_hash)
|
||||
|
||||
|
@ -92,11 +125,19 @@ module Gitlab
|
|||
end
|
||||
|
||||
def member_hash(member)
|
||||
parsed_hash(member).merge(
|
||||
result = parsed_hash(member).merge(
|
||||
'source_id' => @importable.id,
|
||||
'importing' => true,
|
||||
'access_level' => [member['access_level'], highest_access_level].min
|
||||
).except('user_id')
|
||||
|
||||
if result['created_by_id']
|
||||
created_by_email = exported_members_email_map[result['created_by_id']]
|
||||
|
||||
result['created_by_id'] = existing_users_email_map[created_by_email]
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def parsed_hash(member)
|
||||
|
@ -104,14 +145,6 @@ module Gitlab
|
|||
relation_class: relation_class)
|
||||
end
|
||||
|
||||
def find_user_query(email)
|
||||
user_arel[:email].eq(email)
|
||||
end
|
||||
|
||||
def user_arel
|
||||
@user_arel ||= User.arel_table
|
||||
end
|
||||
|
||||
def relation_class
|
||||
case @importable
|
||||
when ::Project
|
||||
|
@ -143,7 +176,7 @@ module Gitlab
|
|||
|
||||
def base_log_params(member_hash)
|
||||
{
|
||||
user_id: member_hash['user']&.id,
|
||||
user_id: member_hash['user_id'],
|
||||
access_level: member_hash['access_level'],
|
||||
importable_type: @importable.class.to_s,
|
||||
importable_id: @importable.id,
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This module is used to return fake strong password for tests
|
||||
|
||||
module Gitlab
|
||||
module Password
|
||||
DEFAULT_LENGTH = 12
|
||||
TEST_DEFAULT = "123qweQWE!@#" + "0" * (User.password_length.max - DEFAULT_LENGTH)
|
||||
def self.test_default(length = 12)
|
||||
password_length = [[User.password_length.min, length].max, User.password_length.max].min
|
||||
TEST_DEFAULT[...password_length]
|
||||
end
|
||||
end
|
||||
end
|
19
lib/gitlab/sanitizers/exception_message.rb
Normal file
19
lib/gitlab/sanitizers/exception_message.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Sanitizers
|
||||
module ExceptionMessage
|
||||
FILTERED_STRING = '[FILTERED]'
|
||||
EXCEPTION_NAMES = %w(URI::InvalidURIError Addressable::URI::InvalidURIError).freeze
|
||||
MESSAGE_REGEX = %r{(\A[^:]+:\s).*\Z}.freeze
|
||||
|
||||
class << self
|
||||
def clean(exception_name, message)
|
||||
return message unless exception_name.in?(EXCEPTION_NAMES)
|
||||
|
||||
message.sub(MESSAGE_REGEX, '\1' + FILTERED_STRING)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -125,7 +125,7 @@ class GroupSeeder
|
|||
name: FFaker::Name.name,
|
||||
email: FFaker::Internet.email,
|
||||
confirmed_at: DateTime.now,
|
||||
password: Gitlab::Password.test_default
|
||||
password: Devise.friendly_token
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -10829,43 +10829,25 @@ msgstr ""
|
|||
msgid "Critical vulnerabilities present"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Contact has been added"
|
||||
msgid "Crm|Contact has been added."
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Contact has been updated"
|
||||
msgid "Crm|Contact has been updated."
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Create new contact"
|
||||
msgid "Crm|Customer relations contacts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Create organization"
|
||||
msgid "Crm|Customer relations organizations"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Customer Relations Contacts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Customer Relations Organizations"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Default rate (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Description (optional)"
|
||||
msgid "Crm|Default rate"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Edit contact"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Email"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|First name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Last name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|New Organization"
|
||||
msgid "Crm|Edit organization"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|New contact"
|
||||
|
@ -10880,10 +10862,10 @@ msgstr ""
|
|||
msgid "Crm|No organizations found"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Organization has been added"
|
||||
msgid "Crm|Organization has been added."
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Phone number (optional)"
|
||||
msgid "Crm|Organization has been updated."
|
||||
msgstr ""
|
||||
|
||||
msgid "Cron Timezone"
|
||||
|
@ -10994,18 +10976,18 @@ msgstr ""
|
|||
msgid "Custom range (UTC)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Customer Relations Contacts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Customer Relations Organizations"
|
||||
msgstr ""
|
||||
|
||||
msgid "Customer experience improvement and third-party offers"
|
||||
msgstr ""
|
||||
|
||||
msgid "Customer relations"
|
||||
msgstr ""
|
||||
|
||||
msgid "Customer relations contacts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Customer relations organizations"
|
||||
msgstr ""
|
||||
|
||||
msgid "Customize CI/CD settings, including Auto DevOps, shared runners, and job artifacts."
|
||||
msgstr ""
|
||||
|
||||
|
@ -13223,6 +13205,9 @@ msgstr ""
|
|||
msgid "Dismissed on pipeline %{pipelineLink} at %{projectLink}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Display"
|
||||
msgstr ""
|
||||
|
||||
msgid "Display alerts from all configured monitoring tools."
|
||||
msgstr ""
|
||||
|
||||
|
@ -27269,6 +27254,9 @@ msgstr ""
|
|||
msgid "Phabricator Tasks"
|
||||
msgstr ""
|
||||
|
||||
msgid "Phone"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pick a name"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -170,7 +170,7 @@
|
|||
"sortablejs": "^1.10.2",
|
||||
"string-hash": "1.1.3",
|
||||
"style-loader": "^2.0.0",
|
||||
"swagger-ui-dist": "^3.52.3",
|
||||
"swagger-ui-dist": "4.8.0",
|
||||
"three": "^0.84.0",
|
||||
"three-orbit-controls": "^82.1.0",
|
||||
"three-stl-loader": "^1.0.4",
|
||||
|
|
|
@ -612,8 +612,8 @@ RSpec.describe Admin::UsersController do
|
|||
end
|
||||
|
||||
context 'when the new password does not match the password confirmation' do
|
||||
let(:password) { Gitlab::Password.test_default }
|
||||
let(:password_confirmation) { "not" + Gitlab::Password.test_default }
|
||||
let(:password) { 'some_password' }
|
||||
let(:password_confirmation) { 'not_same_as_password' }
|
||||
|
||||
it 'shows the edit page again' do
|
||||
update_password(user, password, password_confirmation)
|
||||
|
|
|
@ -186,6 +186,7 @@ RSpec.describe Projects::MergeRequests::CreationsController do
|
|||
|
||||
it 'fetches the commit if a user has access' do
|
||||
expect(Ability).to receive(:allowed?).with(user, :read_project, project) { true }
|
||||
expect(Ability).to receive(:allowed?).with(user, :create_merge_request_in, project) { true }.at_least(:once)
|
||||
|
||||
get :branch_to,
|
||||
params: {
|
||||
|
@ -199,8 +200,25 @@ RSpec.describe Projects::MergeRequests::CreationsController do
|
|||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'does not load the commit when the user cannot create_merge_request_in' do
|
||||
expect(Ability).to receive(:allowed?).with(user, :read_project, project) { true }
|
||||
expect(Ability).to receive(:allowed?).with(user, :create_merge_request_in, project) { false }.at_least(:once)
|
||||
|
||||
get :branch_to,
|
||||
params: {
|
||||
namespace_id: fork_project.namespace,
|
||||
project_id: fork_project,
|
||||
target_project_id: project.id,
|
||||
ref: 'master'
|
||||
}
|
||||
|
||||
expect(assigns(:commit)).to be_nil
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'does not load the commit when the user cannot read the project' do
|
||||
expect(Ability).to receive(:allowed?).with(user, :read_project, project) { false }
|
||||
expect(Ability).to receive(:allowed?).with(user, :create_merge_request_in, project) { true }.at_least(:once)
|
||||
|
||||
get :branch_to,
|
||||
params: {
|
||||
|
|
|
@ -177,6 +177,7 @@ RSpec.describe Projects::MirrorsController do
|
|||
INVALID
|
||||
git@example.com:foo/bar.git
|
||||
ssh://git@example.com:foo/bar.git
|
||||
ssh://127.0.0.1/foo/bar.git
|
||||
].each do |url|
|
||||
it "returns an error with a 400 response for URL #{url.inspect}" do
|
||||
do_get(project, url)
|
||||
|
|
|
@ -521,7 +521,7 @@ RSpec.describe RegistrationsController do
|
|||
end
|
||||
|
||||
it 'succeeds if password is confirmed' do
|
||||
post :destroy, params: { password: Gitlab::Password.test_default }
|
||||
post :destroy, params: { password: '12345678' }
|
||||
|
||||
expect_success
|
||||
end
|
||||
|
@ -562,7 +562,7 @@ RSpec.describe RegistrationsController do
|
|||
end
|
||||
|
||||
it 'fails' do
|
||||
delete :destroy, params: { password: Gitlab::Password.test_default }
|
||||
delete :destroy, params: { password: '12345678' }
|
||||
|
||||
expect_failure(s_('Profiles|You must transfer ownership or delete groups you are an owner of before you can delete your account'))
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@ FactoryBot.define do
|
|||
email { generate(:email) }
|
||||
name { generate(:name) }
|
||||
username { generate(:username) }
|
||||
password { Gitlab::Password.test_default }
|
||||
password { "12345678" }
|
||||
role { 'software_developer' }
|
||||
confirmed_at { Time.now }
|
||||
confirmation_token { nil }
|
||||
|
@ -27,6 +27,10 @@ FactoryBot.define do
|
|||
after(:build) { |user, _| user.block! }
|
||||
end
|
||||
|
||||
trait :disallowed_password do
|
||||
password { User::DISALLOWED_PASSWORDS.first }
|
||||
end
|
||||
|
||||
trait :blocked_pending_approval do
|
||||
after(:build) { |user, _| user.block_pending_approval! }
|
||||
end
|
||||
|
|
55
spec/features/markdown/kroki_spec.rb
Normal file
55
spec/features/markdown/kroki_spec.rb
Normal file
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'kroki rendering', :js do
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
|
||||
before do
|
||||
stub_application_setting(kroki_enabled: true, kroki_url: 'http://localhost:8000')
|
||||
end
|
||||
|
||||
it 'shows kroki image' do
|
||||
plain_text = 'This text length is ignored. ' * 300
|
||||
|
||||
description = <<~KROKI
|
||||
#{plain_text}
|
||||
```plantuml
|
||||
A -> A: T
|
||||
```
|
||||
KROKI
|
||||
|
||||
issue = create(:issue, project: project, description: description)
|
||||
|
||||
visit project_issue_path(project, issue)
|
||||
|
||||
within('.description') do
|
||||
expect(page).to have_css('img')
|
||||
expect(page).not_to have_text 'Warning: Displaying this diagram might cause performance issues on this page.'
|
||||
end
|
||||
end
|
||||
|
||||
it 'hides kroki image and shows warning alert when kroki source size is large' do
|
||||
plantuml_text = 'A -> A: T ' * 300
|
||||
|
||||
description = <<~KROKI
|
||||
```plantuml
|
||||
#{plantuml_text}
|
||||
```
|
||||
KROKI
|
||||
|
||||
issue = create(:issue, project: project, description: description)
|
||||
|
||||
visit project_issue_path(project, issue)
|
||||
|
||||
within('.description') do
|
||||
expect(page).not_to have_css('img')
|
||||
expect(page).to have_text 'Warning: Displaying this diagram might cause performance issues on this page.'
|
||||
|
||||
click_button 'Display'
|
||||
|
||||
expect(page).to have_css('img')
|
||||
expect(page).not_to have_text 'Warning: Displaying this diagram might cause performance issues on this page.'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -44,8 +44,8 @@ RSpec.describe 'Password reset' do
|
|||
|
||||
visit(edit_user_password_path(reset_password_token: token))
|
||||
|
||||
fill_in 'New password', with: "new" + Gitlab::Password.test_default
|
||||
fill_in 'Confirm new password', with: "new" + Gitlab::Password.test_default
|
||||
fill_in 'New password', with: 'hello1234'
|
||||
fill_in 'Confirm new password', with: 'hello1234'
|
||||
|
||||
click_button 'Change your password'
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ RSpec.describe 'Profile account page', :js do
|
|||
it 'deletes user', :js, :sidekiq_might_not_need_inline do
|
||||
click_button 'Delete account'
|
||||
|
||||
fill_in 'password', with: Gitlab::Password.test_default
|
||||
fill_in 'password', with: '12345678'
|
||||
|
||||
page.within '.modal' do
|
||||
click_button 'Delete account'
|
||||
|
|
|
@ -39,7 +39,7 @@ RSpec.describe 'Profile > Password' do
|
|||
|
||||
describe 'User puts the same passwords in the field and in the confirmation' do
|
||||
it 'shows a success message' do
|
||||
fill_passwords(Gitlab::Password.test_default, Gitlab::Password.test_default)
|
||||
fill_passwords('mypassword', 'mypassword')
|
||||
|
||||
page.within('[data-testid="alert-info"]') do
|
||||
expect(page).to have_content('Password was successfully updated. Please sign in again.')
|
||||
|
@ -79,7 +79,7 @@ RSpec.describe 'Profile > Password' do
|
|||
end
|
||||
|
||||
context 'Change password' do
|
||||
let(:new_password) { "new" + Gitlab::Password.test_default }
|
||||
let(:new_password) { '22233344' }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
|
@ -170,8 +170,8 @@ RSpec.describe 'Profile > Password' do
|
|||
expect(page).to have_current_path new_profile_password_path, ignore_query: true
|
||||
|
||||
fill_in :user_password, with: user.password
|
||||
fill_in :user_new_password, with: Gitlab::Password.test_default
|
||||
fill_in :user_password_confirmation, with: Gitlab::Password.test_default
|
||||
fill_in :user_new_password, with: '12345678'
|
||||
fill_in :user_password_confirmation, with: '12345678'
|
||||
click_button 'Set new password'
|
||||
|
||||
expect(page).to have_current_path new_user_session_path, ignore_query: true
|
||||
|
|
|
@ -948,6 +948,53 @@ RSpec.describe 'File blob', :js do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'openapi.yml' do
|
||||
before do
|
||||
file_name = 'openapi.yml'
|
||||
|
||||
create_file(file_name, '
|
||||
swagger: \'2.0\'
|
||||
info:
|
||||
title: Classic API Resource Documentation
|
||||
description: |
|
||||
<div class="foo-bar" style="background-color: red;" data-foo-bar="baz">
|
||||
<h1>Swagger API documentation</h1>
|
||||
</div>
|
||||
version: production
|
||||
basePath: /JSSResource/
|
||||
produces:
|
||||
- application/xml
|
||||
- application/json
|
||||
consumes:
|
||||
- application/xml
|
||||
- application/json
|
||||
security:
|
||||
- basicAuth: []
|
||||
paths:
|
||||
/accounts:
|
||||
get:
|
||||
responses:
|
||||
\'200\':
|
||||
description: No response was specified
|
||||
tags:
|
||||
- accounts
|
||||
operationId: findAccounts
|
||||
summary: Finds all accounts
|
||||
')
|
||||
visit_blob(file_name, useUnsafeMarkdown: '1')
|
||||
click_button('Display rendered file')
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'removes `style`, `class`, and `data-*`` attributes from HTML' do
|
||||
expect(page).to have_css('h1', text: 'Swagger API documentation')
|
||||
expect(page).not_to have_css('.foo-bar')
|
||||
expect(page).not_to have_css('[style="background-color: red;"]')
|
||||
expect(page).not_to have_css('[data-foo-bar="baz"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'realtime pipelines' do
|
||||
|
|
|
@ -1050,6 +1050,53 @@ RSpec.describe 'File blob', :js do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'openapi.yml' do
|
||||
before do
|
||||
file_name = 'openapi.yml'
|
||||
|
||||
create_file(file_name, '
|
||||
swagger: \'2.0\'
|
||||
info:
|
||||
title: Classic API Resource Documentation
|
||||
description: |
|
||||
<div class="foo-bar" style="background-color: red;" data-foo-bar="baz">
|
||||
<h1>Swagger API documentation</h1>
|
||||
</div>
|
||||
version: production
|
||||
basePath: /JSSResource/
|
||||
produces:
|
||||
- application/xml
|
||||
- application/json
|
||||
consumes:
|
||||
- application/xml
|
||||
- application/json
|
||||
security:
|
||||
- basicAuth: []
|
||||
paths:
|
||||
/accounts:
|
||||
get:
|
||||
responses:
|
||||
\'200\':
|
||||
description: No response was specified
|
||||
tags:
|
||||
- accounts
|
||||
operationId: findAccounts
|
||||
summary: Finds all accounts
|
||||
')
|
||||
visit_blob(file_name, useUnsafeMarkdown: '1')
|
||||
click_button('Display rendered file')
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'removes `style`, `class`, and `data-*`` attributes from HTML' do
|
||||
expect(page).to have_css('h1', text: 'Swagger API documentation')
|
||||
expect(page).not_to have_css('.foo-bar')
|
||||
expect(page).not_to have_css('[style="background-color: red;"]')
|
||||
expect(page).not_to have_css('[data-foo-bar="baz"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'realtime pipelines' do
|
||||
|
|
|
@ -9,7 +9,7 @@ RSpec.describe 'Session TTLs', :clean_gitlab_redis_shared_state do
|
|||
visit new_user_session_path
|
||||
# The session key only gets created after a post
|
||||
fill_in 'user_login', with: 'non-existant@gitlab.org'
|
||||
fill_in 'user_password', with: Gitlab::Password.test_default
|
||||
fill_in 'user_password', with: '12345678'
|
||||
click_button 'Sign in'
|
||||
|
||||
expect(page).to have_content('Invalid login or password')
|
||||
|
|
|
@ -49,15 +49,15 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
|
|||
expect(page).to have_current_path edit_user_password_path, ignore_query: true
|
||||
expect(page).to have_content('Please create a password for your new account.')
|
||||
|
||||
fill_in 'user_password', with: Gitlab::Password.test_default
|
||||
fill_in 'user_password_confirmation', with: Gitlab::Password.test_default
|
||||
fill_in 'user_password', with: 'password'
|
||||
fill_in 'user_password_confirmation', with: 'password'
|
||||
click_button 'Change your password'
|
||||
|
||||
expect(page).to have_current_path new_user_session_path, ignore_query: true
|
||||
expect(page).to have_content(I18n.t('devise.passwords.updated_not_active'))
|
||||
|
||||
fill_in 'user_login', with: user.username
|
||||
fill_in 'user_password', with: Gitlab::Password.test_default
|
||||
fill_in 'user_password', with: 'password'
|
||||
click_button 'Sign in'
|
||||
|
||||
expect_single_session_with_authenticated_ttl
|
||||
|
@ -150,6 +150,27 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'with a disallowed password' do
|
||||
let(:user) { create(:user, :disallowed_password) }
|
||||
|
||||
before do
|
||||
expect(authentication_metrics)
|
||||
.to increment(:user_unauthenticated_counter)
|
||||
.and increment(:user_password_invalid_counter)
|
||||
end
|
||||
|
||||
it 'disallows login' do
|
||||
gitlab_sign_in(user, password: user.password)
|
||||
|
||||
expect(page).to have_content('Invalid login or password.')
|
||||
end
|
||||
|
||||
it 'does not update Devise trackable attributes' do
|
||||
expect { gitlab_sign_in(user, password: user.password) }
|
||||
.not_to change { User.ghost.reload.sign_in_count }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with the ghost user' do
|
||||
it 'disallows login' do
|
||||
expect(authentication_metrics)
|
||||
|
@ -210,7 +231,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
|
|||
end
|
||||
|
||||
it 'does not allow sign-in if the user password is updated before entering a one-time code' do
|
||||
user.update!(password: "new" + Gitlab::Password.test_default)
|
||||
user.update!(password: 'new_password')
|
||||
|
||||
enter_code(user.current_otp)
|
||||
|
||||
|
@ -447,7 +468,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
|
|||
visit new_user_session_path
|
||||
|
||||
fill_in 'user_login', with: user.email
|
||||
fill_in 'user_password', with: Gitlab::Password.test_default
|
||||
fill_in 'user_password', with: '12345678'
|
||||
click_button 'Sign in'
|
||||
|
||||
expect(page).to have_current_path(new_profile_password_path, ignore_query: true)
|
||||
|
@ -456,7 +477,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
|
|||
end
|
||||
|
||||
context 'with invalid username and password' do
|
||||
let(:user) { create(:user, password: "not" + Gitlab::Password.test_default) }
|
||||
let(:user) { create(:user, password: 'not-the-default') }
|
||||
|
||||
it 'blocks invalid login' do
|
||||
expect(authentication_metrics)
|
||||
|
@ -767,7 +788,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
|
|||
visit new_user_session_path
|
||||
|
||||
fill_in 'user_login', with: user.email
|
||||
fill_in 'user_password', with: Gitlab::Password.test_default
|
||||
fill_in 'user_password', with: '12345678'
|
||||
|
||||
click_button 'Sign in'
|
||||
|
||||
|
@ -788,7 +809,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
|
|||
visit new_user_session_path
|
||||
|
||||
fill_in 'user_login', with: user.email
|
||||
fill_in 'user_password', with: Gitlab::Password.test_default
|
||||
fill_in 'user_password', with: '12345678'
|
||||
|
||||
click_button 'Sign in'
|
||||
|
||||
|
@ -810,7 +831,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
|
|||
visit new_user_session_path
|
||||
|
||||
fill_in 'user_login', with: user.email
|
||||
fill_in 'user_password', with: Gitlab::Password.test_default
|
||||
fill_in 'user_password', with: '12345678'
|
||||
|
||||
click_button 'Sign in'
|
||||
|
||||
|
@ -845,7 +866,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
|
|||
visit new_user_session_path
|
||||
|
||||
fill_in 'user_login', with: user.email
|
||||
fill_in 'user_password', with: Gitlab::Password.test_default
|
||||
fill_in 'user_password', with: '12345678'
|
||||
click_button 'Sign in'
|
||||
|
||||
fill_in 'user_otp_attempt', with: user.reload.current_otp
|
||||
|
@ -871,7 +892,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
|
|||
visit new_user_session_path
|
||||
|
||||
fill_in 'user_login', with: user.email
|
||||
fill_in 'user_password', with: Gitlab::Password.test_default
|
||||
fill_in 'user_password', with: '12345678'
|
||||
click_button 'Sign in'
|
||||
|
||||
expect_to_be_on_terms_page
|
||||
|
@ -879,7 +900,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
|
|||
|
||||
expect(page).to have_current_path(new_profile_password_path, ignore_query: true)
|
||||
|
||||
fill_in 'user_password', with: Gitlab::Password.test_default
|
||||
fill_in 'user_password', with: '12345678'
|
||||
fill_in 'user_new_password', with: 'new password'
|
||||
fill_in 'user_password_confirmation', with: 'new password'
|
||||
click_button 'Set new password'
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue