Merge commit '83f0798e7dc588f0e4cb6816daadeef7dbfc8b81' into fix/gb/encrypt-runners-tokens
* commit '83f0798e7dc588f0e4cb6816daadeef7dbfc8b81': (101 commits)
This commit is contained in:
commit
6b4926f2aa
|
|
@ -962,6 +962,7 @@ review-deploy:
|
|||
GITLAB_ADMIN_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}"
|
||||
GITHUB_ACCESS_TOKEN: "${REVIEW_APPS_QA_GITHUB_ACCESS_TOKEN}"
|
||||
EE_LICENSE: "${REVIEW_APPS_EE_LICENSE}"
|
||||
QA_DEBUG: "true"
|
||||
artifacts:
|
||||
paths:
|
||||
- ./qa/gitlab-qa-run-*
|
||||
|
|
@ -977,6 +978,7 @@ review-deploy:
|
|||
|
||||
review-qa-smoke:
|
||||
<<: *review-qa-base
|
||||
retry: 2
|
||||
script:
|
||||
- gitlab-qa Test::Instance::Smoke "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ Set the title to: `[Security] Description of the original issue`
|
|||
- [ ] Add a link to the MR to the [links section](#links)
|
||||
- [ ] Add a link to an EE MR if required
|
||||
- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**.
|
||||
- [ ] Add a link to this issue on the original security issue.
|
||||
|
||||
#### Backports
|
||||
|
||||
|
|
@ -37,6 +38,7 @@ Set the title to: `[Security] Description of the original issue`
|
|||
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
|
||||
- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
|
||||
- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
|
||||
- [ ] Once your `master` MR is merged, comment on the original security issue with a link to that MR indicating the issue is fixed.
|
||||
|
||||
### Summary
|
||||
|
||||
|
|
|
|||
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -2,6 +2,22 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 11.5.2 (2018-12-03)
|
||||
|
||||
### Removed (1 change)
|
||||
|
||||
- Removed Site Statistics optimization as it was causing problems. !23314
|
||||
|
||||
### Fixed (6 changes, 1 of them is from the community)
|
||||
|
||||
- Display impersonation token value only after creation. !22916
|
||||
- Fix not render emoji in filter dropdown. !23112 (Hiroyuki Sato)
|
||||
- Fixes stuck tooltip on stop env button. !23244
|
||||
- Correctly handle data-loss scenarios when encrypting columns. !23306
|
||||
- Clear BatchLoader context between Sidekiq jobs. !23308
|
||||
- Fix handling of filenames with hash characters in tree view. !23368
|
||||
|
||||
|
||||
## 11.5.1 (2018-11-26)
|
||||
|
||||
### Security (17 changes)
|
||||
|
|
@ -287,6 +303,14 @@ entry.
|
|||
- Disables stop environment button while the deploy is in progress.
|
||||
|
||||
|
||||
## 11.4.9 (2018-12-03)
|
||||
|
||||
### Fixed (2 changes)
|
||||
|
||||
- Display impersonation token value only after creation. !22916
|
||||
- Correctly handle data-loss scenarios when encrypting columns. !23306
|
||||
|
||||
|
||||
## 11.4.8 (2018-11-27)
|
||||
|
||||
### Security (24 changes)
|
||||
|
|
|
|||
|
|
@ -181,4 +181,4 @@ This [documentation](doc/development/contributing/merge_request_workflow.md) has
|
|||
|
||||
## Style guides
|
||||
|
||||
This [documentation](doc/development/contributing/design.md) has been moved.
|
||||
This [documentation](doc/development/contributing/style_guides.md) has been moved.
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1.1.0
|
||||
1.2.0
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
7.2.1
|
||||
7.3.0
|
||||
|
|
|
|||
6
Gemfile
6
Gemfile
|
|
@ -82,7 +82,7 @@ gem 'validates_hostname', '~> 1.0.6'
|
|||
gem 'browser', '~> 2.5'
|
||||
|
||||
# GPG
|
||||
gem 'gpgme'
|
||||
gem 'gpgme', '~> 2.0.18'
|
||||
|
||||
# LDAP Auth
|
||||
# GitLab fork with several improvements to original library. For full list of changes
|
||||
|
|
@ -91,7 +91,7 @@ gem 'gitlab_omniauth-ldap', '~> 2.0.4', require: 'omniauth-ldap'
|
|||
gem 'net-ldap'
|
||||
|
||||
# API
|
||||
gem 'grape', '~> 1.1'
|
||||
gem 'grape', '~> 1.1.0'
|
||||
gem 'grape-entity', '~> 0.7.1'
|
||||
gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
|
||||
|
||||
|
|
@ -432,7 +432,7 @@ group :ed25519 do
|
|||
end
|
||||
|
||||
# Gitaly GRPC client
|
||||
gem 'gitaly-proto', '~> 1.1.0', require: 'gitaly'
|
||||
gem 'gitaly-proto', '~> 1.2.0', require: 'gitaly'
|
||||
gem 'grpc', '~> 1.15.0'
|
||||
|
||||
gem 'google-protobuf', '~> 3.6'
|
||||
|
|
|
|||
12
Gemfile.lock
12
Gemfile.lock
|
|
@ -273,7 +273,7 @@ GEM
|
|||
gettext_i18n_rails (>= 0.7.1)
|
||||
po_to_json (>= 1.0.0)
|
||||
rails (>= 3.2.0)
|
||||
gitaly-proto (1.1.0)
|
||||
gitaly-proto (1.2.0)
|
||||
grpc (~> 1.0)
|
||||
github-markup (1.7.0)
|
||||
gitlab-default_value_for (3.1.1)
|
||||
|
|
@ -313,8 +313,8 @@ GEM
|
|||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.7)
|
||||
gpgme (2.0.13)
|
||||
mini_portile2 (~> 2.1)
|
||||
gpgme (2.0.18)
|
||||
mini_portile2 (~> 2.3)
|
||||
grape (1.1.0)
|
||||
activesupport
|
||||
builder
|
||||
|
|
@ -1006,7 +1006,7 @@ DEPENDENCIES
|
|||
gettext (~> 3.2.2)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.3)
|
||||
gitaly-proto (~> 1.1.0)
|
||||
gitaly-proto (~> 1.2.0)
|
||||
github-markup (~> 1.7.0)
|
||||
gitlab-default_value_for (~> 3.1.1)
|
||||
gitlab-markup (~> 1.6.5)
|
||||
|
|
@ -1016,8 +1016,8 @@ DEPENDENCIES
|
|||
gon (~> 6.2)
|
||||
google-api-client (~> 0.23)
|
||||
google-protobuf (~> 3.6)
|
||||
gpgme
|
||||
grape (~> 1.1)
|
||||
gpgme (~> 2.0.18)
|
||||
grape (~> 1.1.0)
|
||||
grape-entity (~> 0.7.1)
|
||||
grape-path-helpers (~> 1.0)
|
||||
grape_logging (~> 1.7)
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ GEM
|
|||
gettext_i18n_rails (>= 0.7.1)
|
||||
po_to_json (>= 1.0.0)
|
||||
rails (>= 3.2.0)
|
||||
gitaly-proto (1.1.0)
|
||||
gitaly-proto (1.2.0)
|
||||
grpc (~> 1.0)
|
||||
github-markup (1.7.0)
|
||||
gitlab-markup (1.6.5)
|
||||
|
|
@ -310,8 +310,8 @@ GEM
|
|||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.7)
|
||||
gpgme (2.0.13)
|
||||
mini_portile2 (~> 2.1)
|
||||
gpgme (2.0.18)
|
||||
mini_portile2 (~> 2.3)
|
||||
grape (1.1.0)
|
||||
activesupport
|
||||
builder
|
||||
|
|
@ -998,7 +998,7 @@ DEPENDENCIES
|
|||
gettext (~> 3.2.2)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.3)
|
||||
gitaly-proto (~> 1.1.0)
|
||||
gitaly-proto (~> 1.2.0)
|
||||
github-markup (~> 1.7.0)
|
||||
gitlab-markup (~> 1.6.5)
|
||||
gitlab-sidekiq-fetcher
|
||||
|
|
@ -1007,8 +1007,8 @@ DEPENDENCIES
|
|||
gon (~> 6.2)
|
||||
google-api-client (~> 0.23)
|
||||
google-protobuf (~> 3.6)
|
||||
gpgme
|
||||
grape (~> 1.1)
|
||||
gpgme (~> 2.0.18)
|
||||
grape (~> 1.1.0)
|
||||
grape-entity (~> 0.7.1)
|
||||
grape-path-helpers (~> 1.0)
|
||||
grape_logging (~> 1.7)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import axios from './lib/utils/axios_utils';
|
|||
const Api = {
|
||||
groupsPath: '/api/:version/groups.json',
|
||||
groupPath: '/api/:version/groups/:id',
|
||||
subgroupsPath: '/api/:version/groups/:id/subgroups',
|
||||
namespacesPath: '/api/:version/namespaces.json',
|
||||
groupProjectsPath: '/api/:version/groups/:id/projects.json',
|
||||
projectsPath: '/api/:version/projects.json',
|
||||
|
|
|
|||
|
|
@ -102,6 +102,12 @@ export default {
|
|||
if (this.shouldShow) {
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
const id = window && window.location && window.location.hash;
|
||||
|
||||
if (id) {
|
||||
this.setHighlightedRow(id.slice(1));
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.adjustView();
|
||||
|
|
@ -114,6 +120,7 @@ export default {
|
|||
'fetchDiffFiles',
|
||||
'startRenderDiffsQueue',
|
||||
'assignDiscussionsToDiff',
|
||||
'setHighlightedRow',
|
||||
]),
|
||||
fetchData() {
|
||||
this.fetchDiffFiles()
|
||||
|
|
|
|||
|
|
@ -72,6 +72,13 @@ export default {
|
|||
diffFiles: state => state.diffs.diffFiles,
|
||||
}),
|
||||
...mapGetters(['isLoggedIn']),
|
||||
lineCode() {
|
||||
return (
|
||||
this.line.line_code ||
|
||||
(this.line.left && this.line.line.left.line_code) ||
|
||||
(this.line.right && this.line.right.line_code)
|
||||
);
|
||||
},
|
||||
lineHref() {
|
||||
return `#${this.line.line_code || ''}`;
|
||||
},
|
||||
|
|
@ -97,7 +104,7 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', ['loadMoreLines', 'showCommentForm']),
|
||||
...mapActions('diffs', ['loadMoreLines', 'showCommentForm', 'setHighlightedRow']),
|
||||
handleCommentButton() {
|
||||
this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
|
||||
},
|
||||
|
|
@ -168,7 +175,13 @@ export default {
|
|||
>
|
||||
<icon :size="12" name="comment" />
|
||||
</button>
|
||||
<a v-if="lineNumber" :data-linenumber="lineNumber" :href="lineHref"> </a>
|
||||
<a
|
||||
v-if="lineNumber"
|
||||
:data-linenumber="lineNumber"
|
||||
:href="lineHref"
|
||||
@click="setHighlightedRow(lineCode);"
|
||||
>
|
||||
</a>
|
||||
<diff-gutter-avatars v-if="shouldShowAvatarsOnGutter" :discussions="line.discussions" />
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import DiffLineGutterContent from './diff_line_gutter_content.vue';
|
||||
import {
|
||||
MATCH_LINE_TYPE,
|
||||
|
|
@ -30,6 +30,11 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isHighlighted: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false,
|
||||
},
|
||||
diffViewType: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
|
@ -85,6 +90,7 @@ export default {
|
|||
const { type } = this.line;
|
||||
|
||||
return {
|
||||
hll: this.isHighlighted,
|
||||
[type]: type,
|
||||
[LINE_UNFOLD_CLASS_NAME]: this.isMatchLine,
|
||||
[LINE_HOVER_CLASS_NAME]:
|
||||
|
|
@ -99,6 +105,7 @@ export default {
|
|||
return this.lineType === OLD_LINE_TYPE ? this.line.old_line : this.line.new_line;
|
||||
},
|
||||
},
|
||||
methods: mapActions('diffs', ['setHighlightedRow']),
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { mapGetters, mapActions, mapState } from 'vuex';
|
||||
import DiffTableCell from './diff_table_cell.vue';
|
||||
import {
|
||||
NEW_LINE_TYPE,
|
||||
|
|
@ -40,6 +40,11 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
isHighlighted(state) {
|
||||
return this.line.line_code !== null && this.line.line_code === state.diffs.highlightedRow;
|
||||
},
|
||||
}),
|
||||
...mapGetters('diffs', ['isInlineView']),
|
||||
isContextLine() {
|
||||
return this.line.type === CONTEXT_LINE_TYPE;
|
||||
|
|
@ -91,6 +96,7 @@ export default {
|
|||
:is-bottom="isBottom"
|
||||
:is-hover="isHover"
|
||||
:show-comment-button="true"
|
||||
:is-highlighted="isHighlighted"
|
||||
class="diff-line-num old_line"
|
||||
/>
|
||||
<diff-table-cell
|
||||
|
|
@ -100,8 +106,18 @@ export default {
|
|||
:line-type="newLineType"
|
||||
:is-bottom="isBottom"
|
||||
:is-hover="isHover"
|
||||
:is-highlighted="isHighlighted"
|
||||
class="diff-line-num new_line qa-new-diff-line"
|
||||
/>
|
||||
<td :class="line.type" class="line_content" v-html="line.rich_text"></td>
|
||||
<td
|
||||
:class="[
|
||||
line.type,
|
||||
{
|
||||
hll: isHighlighted,
|
||||
},
|
||||
]"
|
||||
class="line_content"
|
||||
v-html="line.rich_text"
|
||||
></td>
|
||||
</tr>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import $ from 'jquery';
|
||||
import DiffTableCell from './diff_table_cell.vue';
|
||||
import {
|
||||
|
|
@ -43,6 +43,15 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
isHighlighted(state) {
|
||||
const lineCode =
|
||||
(this.line.left && this.line.left.line_code) ||
|
||||
(this.line.right && this.line.right.line_code);
|
||||
|
||||
return lineCode ? lineCode === state.diffs.highlightedRow : false;
|
||||
},
|
||||
}),
|
||||
isContextLine() {
|
||||
return this.line.left && this.line.left.type === CONTEXT_LINE_TYPE;
|
||||
},
|
||||
|
|
@ -57,7 +66,14 @@ export default {
|
|||
return OLD_NO_NEW_LINE_TYPE;
|
||||
}
|
||||
|
||||
return this.line.left ? this.line.left.type : EMPTY_CELL_TYPE;
|
||||
const lineTypeClass = this.line.left ? this.line.left.type : EMPTY_CELL_TYPE;
|
||||
|
||||
return [
|
||||
lineTypeClass,
|
||||
{
|
||||
hll: this.isHighlighted,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
created() {
|
||||
|
|
@ -114,6 +130,7 @@ export default {
|
|||
:line-type="oldLineType"
|
||||
:is-bottom="isBottom"
|
||||
:is-hover="isLeftHover"
|
||||
:is-highlighted="isHighlighted"
|
||||
:show-comment-button="true"
|
||||
:diff-view-type="parallelDiffViewType"
|
||||
line-position="left"
|
||||
|
|
@ -139,6 +156,7 @@ export default {
|
|||
:line-type="newLineType"
|
||||
:is-bottom="isBottom"
|
||||
:is-hover="isRightHover"
|
||||
:is-highlighted="isHighlighted"
|
||||
:show-comment-button="true"
|
||||
:diff-view-type="parallelDiffViewType"
|
||||
line-position="right"
|
||||
|
|
@ -146,7 +164,12 @@ export default {
|
|||
/>
|
||||
<td
|
||||
:id="line.right.line_code"
|
||||
:class="line.right.type"
|
||||
:class="[
|
||||
line.right.type,
|
||||
{
|
||||
hll: isHighlighted,
|
||||
},
|
||||
]"
|
||||
class="line_content parallel right-side"
|
||||
@mousedown.native="handleParallelLineMouseDown"
|
||||
v-html="line.right.rich_text"
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ export const fetchDiffFiles = ({ state, commit }) => {
|
|||
.then(handleLocationHash);
|
||||
};
|
||||
|
||||
export const setHighlightedRow = ({ commit }, lineCode) => {
|
||||
commit(types.SET_HIGHLIGHTED_ROW, lineCode);
|
||||
};
|
||||
|
||||
// This is adding line discussions to the actual lines in the diff tree
|
||||
// once for parallel and once for inline mode
|
||||
export const assignDiscussionsToDiff = (
|
||||
|
|
@ -127,7 +131,7 @@ export const loadMoreLines = ({ commit }, options) => {
|
|||
export const scrollToLineIfNeededInline = (_, line) => {
|
||||
const hash = getLocationHash();
|
||||
|
||||
if (hash && line.lineCode === hash) {
|
||||
if (hash && line.line_code === hash) {
|
||||
handleLocationHash();
|
||||
}
|
||||
};
|
||||
|
|
@ -137,7 +141,7 @@ export const scrollToLineIfNeededParallel = (_, line) => {
|
|||
|
||||
if (
|
||||
hash &&
|
||||
((line.left && line.left.lineCode === hash) || (line.right && line.right.lineCode === hash))
|
||||
((line.left && line.left.line_code === hash) || (line.right && line.right.line_code === hash))
|
||||
) {
|
||||
handleLocationHash();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,4 +26,5 @@ export default () => ({
|
|||
currentDiffFileId: '',
|
||||
projectPath: '',
|
||||
commentForms: [],
|
||||
highlightedRow: null,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,3 +17,4 @@ export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID';
|
|||
export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM';
|
||||
export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM';
|
||||
export const CLOSE_DIFF_FILE_COMMENT_FORM = 'CLOSE_DIFF_FILE_COMMENT_FORM';
|
||||
export const SET_HIGHLIGHTED_ROW = 'SET_HIGHLIGHTED_ROW';
|
||||
|
|
|
|||
|
|
@ -241,4 +241,7 @@ export default {
|
|||
[types.CLOSE_DIFF_FILE_COMMENT_FORM](state, fileHash) {
|
||||
state.commentForms = state.commentForms.filter(form => form.fileHash !== fileHash);
|
||||
},
|
||||
[types.SET_HIGHLIGHTED_ROW](state, lineCode) {
|
||||
state.highlightedRow = lineCode;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -88,10 +88,15 @@ export const conditions = [
|
|||
value: 'started',
|
||||
},
|
||||
{
|
||||
url: 'label_name[]=No+Label',
|
||||
url: 'label_name[]=None',
|
||||
tokenKey: 'label',
|
||||
value: 'none',
|
||||
},
|
||||
{
|
||||
url: 'label_name[]=Any',
|
||||
tokenKey: 'any',
|
||||
value: 'any',
|
||||
},
|
||||
{
|
||||
url: 'my_reaction_emoji=None',
|
||||
tokenKey: 'my-reaction',
|
||||
|
|
|
|||
|
|
@ -10,13 +10,18 @@ export default function groupsSelect() {
|
|||
const $select = $(this);
|
||||
const allAvailable = $select.data('allAvailable');
|
||||
const skipGroups = $select.data('skipGroups') || [];
|
||||
const parentGroupID = $select.data('parentId');
|
||||
const groupsPath = parentGroupID
|
||||
? Api.subgroupsPath.replace(':id', parentGroupID)
|
||||
: Api.groupsPath;
|
||||
|
||||
$select.select2({
|
||||
placeholder: 'Search for a group',
|
||||
allowClear: $select.hasClass('allowClear'),
|
||||
multiple: $select.hasClass('multiselect'),
|
||||
minimumInputLength: 0,
|
||||
ajax: {
|
||||
url: Api.buildUrl(Api.groupsPath),
|
||||
url: Api.buildUrl(groupsPath),
|
||||
dataType: 'json',
|
||||
quietMillis: 250,
|
||||
transport(params) {
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ export default {
|
|||
:key="tabView.name"
|
||||
class="h-100"
|
||||
>
|
||||
<component :is="tabView.name" />
|
||||
<component :is="tabView.component || tabView.name" />
|
||||
</div>
|
||||
</resizable-panel>
|
||||
<nav class="ide-activity-bar">
|
||||
|
|
|
|||
|
|
@ -17,27 +17,29 @@ export function getParameterValues(sParam) {
|
|||
// @param {Object} params - url keys and value to merge
|
||||
// @param {String} url
|
||||
export function mergeUrlParams(params, url) {
|
||||
let newUrl = Object.keys(params).reduce((acc, paramName) => {
|
||||
const paramValue = encodeURIComponent(params[paramName]);
|
||||
const pattern = new RegExp(`\\b(${paramName}=).*?(&|$)`);
|
||||
const re = /^([^?#]*)(\?[^#]*)?(.*)/;
|
||||
const merged = {};
|
||||
const urlparts = url.match(re);
|
||||
|
||||
if (paramValue === null) {
|
||||
return acc.replace(pattern, '');
|
||||
} else if (url.search(pattern) !== -1) {
|
||||
return acc.replace(pattern, `$1${paramValue}$2`);
|
||||
}
|
||||
|
||||
return `${acc}${acc.indexOf('?') > 0 ? '&' : '?'}${paramName}=${paramValue}`;
|
||||
}, decodeURIComponent(url));
|
||||
|
||||
// Remove a trailing ampersand
|
||||
const lastChar = newUrl[newUrl.length - 1];
|
||||
|
||||
if (lastChar === '&') {
|
||||
newUrl = newUrl.slice(0, -1);
|
||||
if (urlparts[2]) {
|
||||
urlparts[2]
|
||||
.substr(1)
|
||||
.split('&')
|
||||
.forEach(part => {
|
||||
if (part.length) {
|
||||
const kv = part.split('=');
|
||||
merged[decodeURIComponent(kv[0])] = decodeURIComponent(kv.slice(1).join('='));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return newUrl;
|
||||
Object.assign(merged, params);
|
||||
|
||||
const query = Object.keys(merged)
|
||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(merged[key])}`)
|
||||
.join('&');
|
||||
|
||||
return `${urlparts[1]}?${query}${urlparts[3]}`;
|
||||
}
|
||||
|
||||
export function removeParamQueryString(url, param) {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export default class MirrorRepos {
|
|||
this.$password.on('input.updateUrl', () => this.debouncedUpdateUrl());
|
||||
|
||||
this.initMirrorSSH();
|
||||
this.updateProtectedBranches();
|
||||
}
|
||||
|
||||
initMirrorSSH() {
|
||||
|
|
|
|||
|
|
@ -105,6 +105,9 @@ export default {
|
|||
deploymentFlagData() {
|
||||
return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
|
||||
},
|
||||
shouldRenderData() {
|
||||
return this.graphData.queries.filter(s => s.result.length > 0).length > 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
hoverData() {
|
||||
|
|
@ -120,17 +123,17 @@ export default {
|
|||
},
|
||||
draw() {
|
||||
const breakpointSize = bp.getBreakpointSize();
|
||||
const query = this.graphData.queries[0];
|
||||
const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width;
|
||||
|
||||
this.margin = measurements.large.margin;
|
||||
|
||||
if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
|
||||
this.graphHeight = 300;
|
||||
this.margin = measurements.small.margin;
|
||||
this.measurements = measurements.small;
|
||||
}
|
||||
this.unitOfDisplay = query.unit || '';
|
||||
|
||||
this.yAxisLabel = this.graphData.y_label || 'Values';
|
||||
this.legendTitle = query.label || 'Average';
|
||||
this.graphWidth = svgWidth - this.margin.left - this.margin.right;
|
||||
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
|
||||
this.baseGraphHeight = this.graphHeight - 50;
|
||||
|
|
@ -139,8 +142,15 @@ export default {
|
|||
// pixel offsets inside the svg and outside are not 1:1
|
||||
this.realPixelRatio = svgWidth / this.baseGraphWidth;
|
||||
|
||||
this.renderAxesPaths();
|
||||
this.formatDeployments();
|
||||
// set the legends on the axes
|
||||
const [query] = this.graphData.queries;
|
||||
this.legendTitle = query ? query.label : 'Average';
|
||||
this.unitOfDisplay = query ? query.unit : '';
|
||||
|
||||
if (this.shouldRenderData) {
|
||||
this.renderAxesPaths();
|
||||
this.formatDeployments();
|
||||
}
|
||||
},
|
||||
handleMouseOverGraph(e) {
|
||||
let point = this.$refs.graphData.createSVGPoint();
|
||||
|
|
@ -266,7 +276,7 @@ export default {
|
|||
:y-axis-label="yAxisLabel"
|
||||
:unit-of-display="unitOfDisplay"
|
||||
/>
|
||||
<svg ref="graphData" :viewBox="innerViewBox" class="graph-data">
|
||||
<svg v-if="shouldRenderData" ref="graphData" :viewBox="innerViewBox" class="graph-data">
|
||||
<slot name="additionalSvgContent" :graphDrawData="graphDrawData" />
|
||||
<graph-path
|
||||
v-for="(path, index) in timeSeries"
|
||||
|
|
@ -293,8 +303,14 @@ export default {
|
|||
@mousemove="handleMouseOverGraph($event);"
|
||||
/>
|
||||
</svg>
|
||||
<svg v-else :viewBox="innerViewBox" class="js-no-data-to-display">
|
||||
<text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle">
|
||||
{{ s__('Metrics|No data to display') }}
|
||||
</text>
|
||||
</svg>
|
||||
</svg>
|
||||
<graph-flag
|
||||
v-if="shouldRenderData"
|
||||
:real-pixel-ratio="realPixelRatio"
|
||||
:current-x-coordinate="currentXCoordinate"
|
||||
:current-data="currentData"
|
||||
|
|
|
|||
|
|
@ -7,10 +7,29 @@ function sortMetrics(metrics) {
|
|||
.value();
|
||||
}
|
||||
|
||||
function checkQueryEmptyData(query) {
|
||||
return {
|
||||
...query,
|
||||
result: query.result.filter(timeSeries => {
|
||||
const newTimeSeries = timeSeries;
|
||||
const hasValue = series =>
|
||||
!Number.isNaN(series.value) && (series.value !== null || series.value !== undefined);
|
||||
const hasNonNullValue = timeSeries.values.find(hasValue);
|
||||
|
||||
newTimeSeries.values = hasNonNullValue ? newTimeSeries.values : [];
|
||||
|
||||
return newTimeSeries.values.length > 0;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function removeTimeSeriesNoData(queries) {
|
||||
return queries.reduce((series, query) => series.concat(checkQueryEmptyData(query)), []);
|
||||
}
|
||||
|
||||
function normalizeMetrics(metrics) {
|
||||
return metrics.map(metric => ({
|
||||
...metric,
|
||||
queries: metric.queries.map(query => ({
|
||||
return metrics.map(metric => {
|
||||
const queries = metric.queries.map(query => ({
|
||||
...query,
|
||||
result: query.result.map(result => ({
|
||||
...result,
|
||||
|
|
@ -19,8 +38,13 @@ function normalizeMetrics(metrics) {
|
|||
value: Number(value),
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
}));
|
||||
|
||||
return {
|
||||
...metric,
|
||||
queries: removeTimeSeriesNoData(queries),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default class MonitoringStore {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
|
|||
import _ from 'underscore';
|
||||
import Autosize from 'autosize';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
||||
import Flash from '../../flash';
|
||||
import Autosave from '../../autosave';
|
||||
import {
|
||||
|
|
@ -30,6 +31,7 @@ export default {
|
|||
markdownField,
|
||||
userAvatarLink,
|
||||
loadingButton,
|
||||
TimelineEntryItem,
|
||||
},
|
||||
mixins: [issuableStateMixin],
|
||||
props: {
|
||||
|
|
@ -309,137 +311,135 @@ Please check your network connection and try again.`;
|
|||
<div>
|
||||
<note-signed-out-widget v-if="!isLoggedIn" />
|
||||
<discussion-locked-widget v-else-if="!canCreateNote" :issuable-type="issuableTypeTitle" />
|
||||
<div v-else-if="canCreateNote" class="notes notes-form timeline">
|
||||
<div class="timeline-entry note-form">
|
||||
<div class="timeline-entry-inner">
|
||||
<div class="flash-container error-alert timeline-content"></div>
|
||||
<div class="timeline-icon d-none d-sm-none d-md-block">
|
||||
<user-avatar-link
|
||||
v-if="author"
|
||||
:link-href="author.path"
|
||||
:img-src="author.avatar_url"
|
||||
:img-alt="author.name"
|
||||
:img-size="40"
|
||||
/>
|
||||
</div>
|
||||
<div class="timeline-content timeline-content-form">
|
||||
<form ref="commentForm" class="new-note common-note-form gfm-form js-main-target-form">
|
||||
<div class="error-alert"></div>
|
||||
<ul v-else-if="canCreateNote" class="notes notes-form timeline">
|
||||
<timeline-entry-item class="note-form">
|
||||
<div class="flash-container error-alert timeline-content"></div>
|
||||
<div class="timeline-icon d-none d-sm-none d-md-block">
|
||||
<user-avatar-link
|
||||
v-if="author"
|
||||
:link-href="author.path"
|
||||
:img-src="author.avatar_url"
|
||||
:img-alt="author.name"
|
||||
:img-size="40"
|
||||
/>
|
||||
</div>
|
||||
<div class="timeline-content timeline-content-form">
|
||||
<form ref="commentForm" class="new-note common-note-form gfm-form js-main-target-form">
|
||||
<div class="error-alert"></div>
|
||||
|
||||
<issue-warning
|
||||
v-if="hasWarning(getNoteableData)"
|
||||
:is-locked="isLocked(getNoteableData)"
|
||||
:is-confidential="isConfidential(getNoteableData)"
|
||||
<issue-warning
|
||||
v-if="hasWarning(getNoteableData)"
|
||||
:is-locked="isLocked(getNoteableData)"
|
||||
:is-confidential="isConfidential(getNoteableData)"
|
||||
/>
|
||||
|
||||
<markdown-field
|
||||
ref="markdownField"
|
||||
:markdown-preview-path="markdownPreviewPath"
|
||||
:markdown-docs-path="markdownDocsPath"
|
||||
:quick-actions-docs-path="quickActionsDocsPath"
|
||||
:markdown-version="markdownVersion"
|
||||
:add-spacing-classes="false"
|
||||
>
|
||||
<textarea
|
||||
id="note-body"
|
||||
ref="textarea"
|
||||
slot="textarea"
|
||||
v-model="note"
|
||||
:disabled="isSubmitting"
|
||||
name="note[note]"
|
||||
class="note-textarea js-vue-comment-form js-note-text
|
||||
js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input"
|
||||
data-supports-quick-actions="true"
|
||||
aria-label="Description"
|
||||
placeholder="Write a comment or drag your files here…"
|
||||
@keydown.up="editCurrentUserLastNote();"
|
||||
@keydown.meta.enter="handleSave();"
|
||||
@keydown.ctrl.enter="handleSave();"
|
||||
>
|
||||
</textarea>
|
||||
</markdown-field>
|
||||
<div class="note-form-actions">
|
||||
<div
|
||||
class="float-left btn-group
|
||||
append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
|
||||
>
|
||||
<button
|
||||
:disabled="isSubmitButtonDisabled"
|
||||
class="btn btn-create comment-btn js-comment-button js-comment-submit-button
|
||||
qa-comment-button"
|
||||
type="submit"
|
||||
@click.prevent="handleSave();"
|
||||
>
|
||||
{{ __(commentButtonTitle) }}
|
||||
</button>
|
||||
<button
|
||||
:disabled="isSubmitButtonDisabled"
|
||||
name="button"
|
||||
type="button"
|
||||
class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
|
||||
data-display="static"
|
||||
data-toggle="dropdown"
|
||||
aria-label="Open comment type dropdown"
|
||||
>
|
||||
<i aria-hidden="true" class="fa fa-caret-down toggle-icon"> </i>
|
||||
</button>
|
||||
|
||||
<ul class="note-type-dropdown dropdown-open-top dropdown-menu">
|
||||
<li :class="{ 'droplab-item-selected': noteType === 'comment' }">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent"
|
||||
@click.prevent="setNoteType('comment');"
|
||||
>
|
||||
<i aria-hidden="true" class="fa fa-check icon"> </i>
|
||||
<div class="description">
|
||||
<strong>Comment</strong>
|
||||
<p>Add a general comment to this {{ noteableDisplayName }}.</p>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="divider droplab-item-ignore"></li>
|
||||
<li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent qa-discussion-option"
|
||||
@click.prevent="setNoteType('discussion');"
|
||||
>
|
||||
<i aria-hidden="true" class="fa fa-check icon"> </i>
|
||||
<div class="description">
|
||||
<strong>Start discussion</strong>
|
||||
<p>{{ startDiscussionDescription }}</p>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<loading-button
|
||||
v-if="canUpdateIssue"
|
||||
:loading="isToggleStateButtonLoading"
|
||||
:container-class="[
|
||||
actionButtonClassNames,
|
||||
'btn btn-comment btn-comment-and-close js-action-button',
|
||||
]"
|
||||
:disabled="isToggleStateButtonLoading || isSubmitting"
|
||||
:label="issueActionButtonTitle"
|
||||
@click="handleSave(true);"
|
||||
/>
|
||||
|
||||
<markdown-field
|
||||
ref="markdownField"
|
||||
:markdown-preview-path="markdownPreviewPath"
|
||||
:markdown-docs-path="markdownDocsPath"
|
||||
:quick-actions-docs-path="quickActionsDocsPath"
|
||||
:markdown-version="markdownVersion"
|
||||
:add-spacing-classes="false"
|
||||
<button
|
||||
v-if="note.length"
|
||||
type="button"
|
||||
class="btn btn-cancel js-note-discard"
|
||||
@click="discard"
|
||||
>
|
||||
<textarea
|
||||
id="note-body"
|
||||
ref="textarea"
|
||||
slot="textarea"
|
||||
v-model="note"
|
||||
:disabled="isSubmitting"
|
||||
name="note[note]"
|
||||
class="note-textarea js-vue-comment-form js-note-text
|
||||
js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input"
|
||||
data-supports-quick-actions="true"
|
||||
aria-label="Description"
|
||||
placeholder="Write a comment or drag your files here…"
|
||||
@keydown.up="editCurrentUserLastNote();"
|
||||
@keydown.meta.enter="handleSave();"
|
||||
@keydown.ctrl.enter="handleSave();"
|
||||
>
|
||||
</textarea>
|
||||
</markdown-field>
|
||||
<div class="note-form-actions">
|
||||
<div
|
||||
class="float-left btn-group
|
||||
append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
|
||||
>
|
||||
<button
|
||||
:disabled="isSubmitButtonDisabled"
|
||||
class="btn btn-create comment-btn js-comment-button js-comment-submit-button
|
||||
qa-comment-button"
|
||||
type="submit"
|
||||
@click.prevent="handleSave();"
|
||||
>
|
||||
{{ __(commentButtonTitle) }}
|
||||
</button>
|
||||
<button
|
||||
:disabled="isSubmitButtonDisabled"
|
||||
name="button"
|
||||
type="button"
|
||||
class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
|
||||
data-display="static"
|
||||
data-toggle="dropdown"
|
||||
aria-label="Open comment type dropdown"
|
||||
>
|
||||
<i aria-hidden="true" class="fa fa-caret-down toggle-icon"> </i>
|
||||
</button>
|
||||
|
||||
<ul class="note-type-dropdown dropdown-open-top dropdown-menu">
|
||||
<li :class="{ 'droplab-item-selected': noteType === 'comment' }">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent"
|
||||
@click.prevent="setNoteType('comment');"
|
||||
>
|
||||
<i aria-hidden="true" class="fa fa-check icon"> </i>
|
||||
<div class="description">
|
||||
<strong>Comment</strong>
|
||||
<p>Add a general comment to this {{ noteableDisplayName }}.</p>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="divider droplab-item-ignore"></li>
|
||||
<li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent qa-discussion-option"
|
||||
@click.prevent="setNoteType('discussion');"
|
||||
>
|
||||
<i aria-hidden="true" class="fa fa-check icon"> </i>
|
||||
<div class="description">
|
||||
<strong>Start discussion</strong>
|
||||
<p>{{ startDiscussionDescription }}</p>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<loading-button
|
||||
v-if="canUpdateIssue"
|
||||
:loading="isToggleStateButtonLoading"
|
||||
:container-class="[
|
||||
actionButtonClassNames,
|
||||
'btn btn-comment btn-comment-and-close js-action-button',
|
||||
]"
|
||||
:disabled="isToggleStateButtonLoading || isSubmitting"
|
||||
:label="issueActionButtonTitle"
|
||||
@click="handleSave(true);"
|
||||
/>
|
||||
|
||||
<button
|
||||
v-if="note.length"
|
||||
type="button"
|
||||
class="btn btn-cancel js-note-discard"
|
||||
@click="discard"
|
||||
>
|
||||
Discard draft
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
Discard draft
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</timeline-entry-item>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -48,13 +48,19 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
resolveDiscussion: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
updatedNoteBody: this.noteBody,
|
||||
conflictWhileEditing: false,
|
||||
isSubmitting: false,
|
||||
isResolving: false,
|
||||
isResolving: this.resolveDiscussion,
|
||||
isUnresolving: !this.resolveDiscussion,
|
||||
resolveAsThread: true,
|
||||
};
|
||||
},
|
||||
|
|
@ -149,7 +155,7 @@ export default {
|
|||
<div ref="editNoteForm" class="note-edit-form current-note-edit-form js-discussion-note-form">
|
||||
<div v-if="conflictWhileEditing" class="js-conflict-edit-warning alert alert-danger">
|
||||
This comment has changed since you started editing, please review the
|
||||
<a :href="noteHash" target="_blank" rel="noopener noreferrer"> updated comment </a> to ensure
|
||||
<a :href="noteHash" target="_blank" rel="noopener noreferrer">updated comment</a> to ensure
|
||||
information is not lost.
|
||||
</div>
|
||||
<div class="flash-container timeline-content"></div>
|
||||
|
|
@ -174,22 +180,20 @@ export default {
|
|||
v-model="updatedNoteBody"
|
||||
:data-supports-quick-actions="!isEditing"
|
||||
name="note[note]"
|
||||
class="note-textarea js-gfm-input js-note-text
|
||||
js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
|
||||
class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
|
||||
aria-label="Description"
|
||||
placeholder="Write a comment or drag your files here…"
|
||||
@keydown.meta.enter="handleUpdate();"
|
||||
@keydown.ctrl.enter="handleUpdate();"
|
||||
@keydown.up="editMyLastNote();"
|
||||
@keydown.esc="cancelHandler(true);"
|
||||
>
|
||||
</textarea>
|
||||
></textarea>
|
||||
</markdown-field>
|
||||
<div class="note-form-actions clearfix">
|
||||
<button
|
||||
:disabled="isDisabled"
|
||||
type="button"
|
||||
class="js-vue-issue-save btn btn-success js-comment-button "
|
||||
class="js-vue-issue-save btn btn-success js-comment-button"
|
||||
@click="handleUpdate();"
|
||||
>
|
||||
{{ saveButtonTitle }}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
<script>
|
||||
import _ from 'underscore';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { GlTooltipDirective } from '@gitlab/ui';
|
||||
import { truncateSha } from '~/lib/utils/text_utility';
|
||||
import { s__, __ } from '~/locale';
|
||||
import { s__, __, sprintf } from '~/locale';
|
||||
import systemNote from '~/vue_shared/components/notes/system_note.vue';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
||||
import Flash from '../../flash';
|
||||
import { SYSTEM_NOTE } from '../constants';
|
||||
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
|
|
@ -36,6 +38,7 @@ export default {
|
|||
placeholderNote,
|
||||
placeholderSystemNote,
|
||||
systemNote,
|
||||
TimelineEntryItem,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
|
@ -156,6 +159,37 @@ export default {
|
|||
(!discussion.diff_discussion && resolved && hasReplies && !isRepliesToggledByUser) || false
|
||||
);
|
||||
},
|
||||
actionText() {
|
||||
const commitId = this.discussion.commit_id ? truncateSha(this.discussion.commit_id) : '';
|
||||
const linkStart = `<a href="${_.escape(this.discussion.discussion_path)}">`;
|
||||
const linkEnd = '</a>';
|
||||
|
||||
let text = s__('MergeRequests|started a discussion');
|
||||
|
||||
if (this.discussion.for_commit) {
|
||||
text = s__(
|
||||
'MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}',
|
||||
);
|
||||
} else if (this.discussion.diff_discussion) {
|
||||
if (this.discussion.active) {
|
||||
text = s__('MergeRequests|started a discussion on %{linkStart}the diff%{linkEnd}');
|
||||
} else {
|
||||
text = s__(
|
||||
'MergeRequests|started a discussion on %{linkStart}an old version of the diff%{linkEnd}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
text,
|
||||
{
|
||||
commitId,
|
||||
linkStart,
|
||||
linkEnd,
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
isReplying() {
|
||||
|
|
@ -269,179 +303,156 @@ Please check your network connection and try again.`;
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<li class="note note-discussion timeline-entry" :class="componentClassName">
|
||||
<div class="timeline-entry-inner">
|
||||
<div class="timeline-content">
|
||||
<div :data-discussion-id="discussion.id" class="discussion js-discussion-container">
|
||||
<div v-if="shouldRenderDiffs" class="discussion-header note-wrapper">
|
||||
<div v-once class="timeline-icon">
|
||||
<user-avatar-link
|
||||
v-if="author"
|
||||
:link-href="author.path"
|
||||
:img-src="author.avatar_url"
|
||||
:img-alt="author.name"
|
||||
:img-size="40"
|
||||
/>
|
||||
</div>
|
||||
<note-header
|
||||
:author="author"
|
||||
:created-at="initialDiscussion.created_at"
|
||||
:note-id="initialDiscussion.id"
|
||||
:include-toggle="true"
|
||||
:expanded="discussion.expanded"
|
||||
@toggleHandler="toggleDiscussionHandler"
|
||||
>
|
||||
<template v-if="discussion.diff_discussion">
|
||||
started a discussion on
|
||||
<a :href="discussion.discussion_path">
|
||||
<template v-if="discussion.active"
|
||||
>the diff</template
|
||||
>
|
||||
<template v-else
|
||||
>an old version of the diff</template
|
||||
>
|
||||
</a>
|
||||
</template>
|
||||
<template v-else-if="discussion.for_commit">
|
||||
started a discussion on commit
|
||||
<a :href="discussion.discussion_path">{{ truncateSha(discussion.commit_id) }}</a>
|
||||
</template>
|
||||
<template v-else
|
||||
>started a discussion</template
|
||||
>
|
||||
</note-header>
|
||||
<note-edited-text
|
||||
v-if="discussion.resolved"
|
||||
:edited-at="discussion.resolved_at"
|
||||
:edited-by="discussion.resolved_by"
|
||||
:action-text="resolvedText"
|
||||
class-name="discussion-headline-light js-discussion-headline"
|
||||
/>
|
||||
<note-edited-text
|
||||
v-else-if="lastUpdatedAt"
|
||||
:edited-at="lastUpdatedAt"
|
||||
:edited-by="lastUpdatedBy"
|
||||
action-text="Last updated"
|
||||
class-name="discussion-headline-light js-discussion-headline"
|
||||
<timeline-entry-item class="note note-discussion" :class="componentClassName">
|
||||
<div class="timeline-content">
|
||||
<div :data-discussion-id="discussion.id" class="discussion js-discussion-container">
|
||||
<div v-if="shouldRenderDiffs" class="discussion-header note-wrapper">
|
||||
<div v-once class="timeline-icon">
|
||||
<user-avatar-link
|
||||
v-if="author"
|
||||
:link-href="author.path"
|
||||
:img-src="author.avatar_url"
|
||||
:img-alt="author.name"
|
||||
:img-size="40"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="shouldShowDiscussions" class="discussion-body">
|
||||
<component
|
||||
:is="wrapperComponent"
|
||||
v-bind="wrapperComponentProps"
|
||||
class="card discussion-wrapper"
|
||||
>
|
||||
<div class="discussion-notes">
|
||||
<ul class="notes">
|
||||
<template v-if="shouldGroupReplies">
|
||||
<component
|
||||
:is="componentName(initialDiscussion)"
|
||||
:note="componentData(initialDiscussion)"
|
||||
@handleDeleteNote="deleteNoteHandler"
|
||||
>
|
||||
<slot slot="avatar-badge" name="avatar-badge"></slot>
|
||||
</component>
|
||||
<toggle-replies-widget
|
||||
v-if="hasReplies"
|
||||
:collapsed="isRepliesCollapsed"
|
||||
:replies="replies"
|
||||
@toggle="toggleReplies"
|
||||
/>
|
||||
<template v-if="!isRepliesCollapsed">
|
||||
<component
|
||||
:is="componentName(note)"
|
||||
v-for="note in replies"
|
||||
:key="note.id"
|
||||
:note="componentData(note)"
|
||||
@handleDeleteNote="deleteNoteHandler"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<note-header
|
||||
:author="author"
|
||||
:created-at="initialDiscussion.created_at"
|
||||
:note-id="initialDiscussion.id"
|
||||
:include-toggle="true"
|
||||
:expanded="discussion.expanded"
|
||||
@toggleHandler="toggleDiscussionHandler"
|
||||
>
|
||||
<span v-html="actionText"></span>
|
||||
</note-header>
|
||||
<note-edited-text
|
||||
v-if="discussion.resolved"
|
||||
:edited-at="discussion.resolved_at"
|
||||
:edited-by="discussion.resolved_by"
|
||||
:action-text="resolvedText"
|
||||
class-name="discussion-headline-light js-discussion-headline"
|
||||
/>
|
||||
<note-edited-text
|
||||
v-else-if="lastUpdatedAt"
|
||||
:edited-at="lastUpdatedAt"
|
||||
:edited-by="lastUpdatedBy"
|
||||
action-text="Last updated"
|
||||
class-name="discussion-headline-light js-discussion-headline"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="shouldShowDiscussions" class="discussion-body">
|
||||
<component
|
||||
:is="wrapperComponent"
|
||||
v-bind="wrapperComponentProps"
|
||||
class="card discussion-wrapper"
|
||||
>
|
||||
<div class="discussion-notes">
|
||||
<ul class="notes">
|
||||
<template v-if="shouldGroupReplies">
|
||||
<component
|
||||
:is="componentName(initialDiscussion)"
|
||||
:note="componentData(initialDiscussion)"
|
||||
@handleDeleteNote="deleteNoteHandler"
|
||||
>
|
||||
<slot slot="avatar-badge" name="avatar-badge"></slot>
|
||||
</component>
|
||||
<toggle-replies-widget
|
||||
v-if="hasReplies"
|
||||
:collapsed="isRepliesCollapsed"
|
||||
:replies="replies"
|
||||
@toggle="toggleReplies"
|
||||
/>
|
||||
<template v-if="!isRepliesCollapsed">
|
||||
<component
|
||||
:is="componentName(note)"
|
||||
v-for="(note, index) in discussion.notes"
|
||||
v-for="note in replies"
|
||||
:key="note.id"
|
||||
:note="componentData(note)"
|
||||
@handleDeleteNote="deleteNoteHandler"
|
||||
>
|
||||
<slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
|
||||
</component>
|
||||
/>
|
||||
</template>
|
||||
</ul>
|
||||
<div
|
||||
v-if="!isRepliesCollapsed"
|
||||
:class="{ 'is-replying': isReplying }"
|
||||
class="discussion-reply-holder"
|
||||
>
|
||||
<template v-if="!isReplying && canReply">
|
||||
<div class="discussion-with-resolve-btn">
|
||||
</template>
|
||||
<template v-else>
|
||||
<component
|
||||
:is="componentName(note)"
|
||||
v-for="(note, index) in discussion.notes"
|
||||
:key="note.id"
|
||||
:note="componentData(note)"
|
||||
@handleDeleteNote="deleteNoteHandler"
|
||||
>
|
||||
<slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
|
||||
</component>
|
||||
</template>
|
||||
</ul>
|
||||
<div
|
||||
v-if="!isRepliesCollapsed"
|
||||
:class="{ 'is-replying': isReplying }"
|
||||
class="discussion-reply-holder"
|
||||
>
|
||||
<template v-if="!isReplying && canReply">
|
||||
<div class="discussion-with-resolve-btn">
|
||||
<button
|
||||
type="button"
|
||||
class="js-vue-discussion-reply btn btn-text-field mr-sm-2 qa-discussion-reply"
|
||||
title="Add a reply"
|
||||
@click="showReplyForm"
|
||||
>
|
||||
Reply...
|
||||
</button>
|
||||
<div v-if="discussion.resolvable">
|
||||
<button
|
||||
type="button"
|
||||
class="js-vue-discussion-reply btn btn-text-field mr-sm-2 qa-discussion-reply"
|
||||
title="Add a reply"
|
||||
@click="showReplyForm"
|
||||
class="btn btn-default mr-sm-2"
|
||||
@click="resolveHandler();"
|
||||
>
|
||||
Reply...
|
||||
<i v-if="isResolving" aria-hidden="true" class="fa fa-spinner fa-spin"></i>
|
||||
{{ resolveButtonTitle }}
|
||||
</button>
|
||||
<div v-if="discussion.resolvable">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default mr-sm-2"
|
||||
@click="resolveHandler();"
|
||||
</div>
|
||||
<div
|
||||
v-if="discussion.resolvable"
|
||||
class="btn-group discussion-actions ml-sm-2"
|
||||
role="group"
|
||||
>
|
||||
<div v-if="!discussionResolved" class="btn-group" role="group">
|
||||
<a
|
||||
v-gl-tooltip
|
||||
:href="discussion.resolve_with_issue_path"
|
||||
:title="s__('MergeRequests|Resolve this discussion in a new issue')"
|
||||
class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"
|
||||
>
|
||||
<i
|
||||
v-if="isResolving"
|
||||
aria-hidden="true"
|
||||
class="fa fa-spinner fa-spin"
|
||||
></i>
|
||||
{{ resolveButtonTitle }}
|
||||
<icon name="issue-new" />
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="hasUnresolvedDiscussions" class="btn-group" role="group">
|
||||
<button
|
||||
v-gl-tooltip
|
||||
class="btn btn-default discussion-next-btn"
|
||||
title="Jump to next unresolved discussion"
|
||||
@click="jumpToNextDiscussion"
|
||||
>
|
||||
<icon name="comment-next" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="discussion.resolvable"
|
||||
class="btn-group discussion-actions ml-sm-2"
|
||||
role="group"
|
||||
>
|
||||
<div v-if="!discussionResolved" class="btn-group" role="group">
|
||||
<a
|
||||
v-gl-tooltip
|
||||
:href="discussion.resolve_with_issue_path"
|
||||
:title="s__('MergeRequests|Resolve this discussion in a new issue')"
|
||||
class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"
|
||||
>
|
||||
<icon name="issue-new" />
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="hasUnresolvedDiscussions" class="btn-group" role="group">
|
||||
<button
|
||||
v-gl-tooltip
|
||||
class="btn btn-default discussion-next-btn"
|
||||
title="Jump to next unresolved discussion"
|
||||
@click="jumpToNextDiscussion"
|
||||
>
|
||||
<icon name="comment-next" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<note-form
|
||||
v-if="isReplying"
|
||||
ref="noteForm"
|
||||
:discussion="discussion"
|
||||
:is-editing="false"
|
||||
save-button-title="Comment"
|
||||
@handleFormUpdate="saveReply"
|
||||
@cancelForm="cancelReplyForm"
|
||||
/>
|
||||
<note-signed-out-widget v-if="!canReply" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<note-form
|
||||
v-if="isReplying"
|
||||
ref="noteForm"
|
||||
:discussion="discussion"
|
||||
:is-editing="false"
|
||||
save-button-title="Comment"
|
||||
@handleFormUpdate="saveReply"
|
||||
@cancelForm="cancelReplyForm"
|
||||
/>
|
||||
<note-signed-out-widget v-if="!canReply" />
|
||||
</div>
|
||||
</component>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</timeline-entry-item>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import $ from 'jquery';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { escape } from 'underscore';
|
||||
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
||||
import Flash from '../../flash';
|
||||
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import noteHeader from './note_header.vue';
|
||||
|
|
@ -18,6 +19,7 @@ export default {
|
|||
noteHeader,
|
||||
noteActions,
|
||||
noteBody,
|
||||
TimelineEntryItem,
|
||||
},
|
||||
mixins: [noteable, resolvable],
|
||||
props: {
|
||||
|
|
@ -169,62 +171,60 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
<timeline-entry-item
|
||||
:id="noteAnchorId"
|
||||
:class="classNameBindings"
|
||||
:data-award-url="note.toggle_award_path"
|
||||
:data-note-id="note.id"
|
||||
class="note timeline-entry note-wrapper"
|
||||
class="note note-wrapper"
|
||||
>
|
||||
<div class="timeline-entry-inner">
|
||||
<div v-once class="timeline-icon">
|
||||
<user-avatar-link
|
||||
:link-href="author.path"
|
||||
:img-src="author.avatar_url"
|
||||
:img-alt="author.name"
|
||||
:img-size="40"
|
||||
>
|
||||
<slot slot="avatar-badge" name="avatar-badge"> </slot>
|
||||
</user-avatar-link>
|
||||
</div>
|
||||
<div class="timeline-content">
|
||||
<div class="note-header">
|
||||
<note-header
|
||||
v-once
|
||||
:author="author"
|
||||
:created-at="note.created_at"
|
||||
:note-id="note.id"
|
||||
action-text="commented"
|
||||
/>
|
||||
<note-actions
|
||||
:author-id="author.id"
|
||||
:note-id="note.id"
|
||||
:note-url="note.noteable_note_url"
|
||||
:access-level="note.human_access"
|
||||
:can-edit="note.current_user.can_edit"
|
||||
:can-award-emoji="note.current_user.can_award_emoji"
|
||||
:can-delete="note.current_user.can_edit"
|
||||
:can-report-as-abuse="canReportAsAbuse"
|
||||
:can-resolve="note.current_user.can_resolve"
|
||||
:report-abuse-path="note.report_abuse_path"
|
||||
:resolvable="note.resolvable"
|
||||
:is-resolved="note.resolved"
|
||||
:is-resolving="isResolving"
|
||||
:resolved-by="note.resolved_by"
|
||||
@handleEdit="editHandler"
|
||||
@handleDelete="deleteHandler"
|
||||
@handleResolve="resolveHandler"
|
||||
/>
|
||||
</div>
|
||||
<note-body
|
||||
ref="noteBody"
|
||||
:note="note"
|
||||
<div v-once class="timeline-icon">
|
||||
<user-avatar-link
|
||||
:link-href="author.path"
|
||||
:img-src="author.avatar_url"
|
||||
:img-alt="author.name"
|
||||
:img-size="40"
|
||||
>
|
||||
<slot slot="avatar-badge" name="avatar-badge"> </slot>
|
||||
</user-avatar-link>
|
||||
</div>
|
||||
<div class="timeline-content">
|
||||
<div class="note-header">
|
||||
<note-header
|
||||
v-once
|
||||
:author="author"
|
||||
:created-at="note.created_at"
|
||||
:note-id="note.id"
|
||||
action-text="commented"
|
||||
/>
|
||||
<note-actions
|
||||
:author-id="author.id"
|
||||
:note-id="note.id"
|
||||
:note-url="note.noteable_note_url"
|
||||
:access-level="note.human_access"
|
||||
:can-edit="note.current_user.can_edit"
|
||||
:is-editing="isEditing"
|
||||
@handleFormUpdate="formUpdateHandler"
|
||||
@cancelForm="formCancelHandler"
|
||||
:can-award-emoji="note.current_user.can_award_emoji"
|
||||
:can-delete="note.current_user.can_edit"
|
||||
:can-report-as-abuse="canReportAsAbuse"
|
||||
:can-resolve="note.current_user.can_resolve"
|
||||
:report-abuse-path="note.report_abuse_path"
|
||||
:resolvable="note.resolvable"
|
||||
:is-resolved="note.resolved"
|
||||
:is-resolving="isResolving"
|
||||
:resolved-by="note.resolved_by"
|
||||
@handleEdit="editHandler"
|
||||
@handleDelete="deleteHandler"
|
||||
@handleResolve="resolveHandler"
|
||||
/>
|
||||
</div>
|
||||
<note-body
|
||||
ref="noteBody"
|
||||
:note="note"
|
||||
:can-edit="note.current_user.can_edit"
|
||||
:is-editing="isEditing"
|
||||
@handleFormUpdate="formUpdateHandler"
|
||||
@cancelForm="formCancelHandler"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</timeline-entry-item>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export default {
|
|||
const discussion = this.resolveAsThread;
|
||||
const endpoint = discussion ? this.discussion.resolve_path : `${this.note.path}/resolve`;
|
||||
|
||||
this.toggleResolveNote({ endpoint, isResolved, discussion })
|
||||
return this.toggleResolveNote({ endpoint, isResolved, discussion })
|
||||
.then(() => {
|
||||
this.isResolving = false;
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import initSettingsPanels from '~/settings_panels';
|
|||
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
|
||||
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
|
||||
import { GROUP_BADGE } from '~/badges/constants';
|
||||
import groupsSelect from '~/groups_select';
|
||||
import projectSelect from '~/project_select';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
|
@ -17,5 +18,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
);
|
||||
mountBadgeSettings(GROUP_BADGE);
|
||||
|
||||
// Initialize Subgroups selector
|
||||
groupsSelect();
|
||||
|
||||
projectSelect();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import bp from '../../../breakpoints';
|
||||
import { slugify } from '../../../lib/utils/text_utility';
|
||||
import { parseQueryStringIntoObject } from '../../../lib/utils/common_utils';
|
||||
import { mergeUrlParams, redirectTo } from '../../../lib/utils/url_utility';
|
||||
|
||||
|
|
@ -26,7 +25,8 @@ export default class Wikis {
|
|||
if (!this.newWikiForm) return;
|
||||
|
||||
const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
|
||||
const slug = slugify(slugInput.value);
|
||||
|
||||
const slug = slugInput.value;
|
||||
|
||||
if (slug.length > 0) {
|
||||
const wikisPath = slugInput.getAttribute('data-wikis-path');
|
||||
|
|
|
|||
|
|
@ -18,23 +18,19 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
graph() {
|
||||
return this.pipeline.details && this.pipeline.details.stages;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
capitalizeStageName(name) {
|
||||
const escapedName = _.escape(name);
|
||||
return escapedName.charAt(0).toUpperCase() + escapedName.slice(1);
|
||||
},
|
||||
|
||||
isFirstColumn(index) {
|
||||
return index === 0;
|
||||
},
|
||||
|
||||
stageConnectorClass(index, stage) {
|
||||
let className;
|
||||
|
||||
|
|
@ -48,7 +44,6 @@ export default {
|
|||
|
||||
return className;
|
||||
},
|
||||
|
||||
refreshPipelineGraph() {
|
||||
this.$emit('refreshPipelineGraph');
|
||||
},
|
||||
|
|
|
|||
|
|
@ -84,10 +84,6 @@ export default {
|
|||
|
||||
return textBuilder.join(' ');
|
||||
},
|
||||
|
||||
tooltipBoundary() {
|
||||
return this.dropdownLength < 5 ? 'viewport' : null;
|
||||
},
|
||||
/**
|
||||
* Verifies if the provided job has an action path
|
||||
*
|
||||
|
|
@ -108,7 +104,7 @@ export default {
|
|||
<div class="ci-job-component">
|
||||
<gl-link
|
||||
v-if="status.has_details"
|
||||
v-gl-tooltip="{ boundary: tooltipBoundary }"
|
||||
v-gl-tooltip
|
||||
:href="status.details_path"
|
||||
:title="tooltipText"
|
||||
:class="cssClassJobName"
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ export default class Star {
|
|||
if (isStarred) {
|
||||
$starSpan.removeClass('starred').text(s__('StarProject|Star'));
|
||||
$startIcon.remove();
|
||||
$this.prepend(spriteIcon('star-o'));
|
||||
$this.prepend(spriteIcon('star-o', 'icon'));
|
||||
} else {
|
||||
$starSpan.addClass('starred').text(__('Unstar'));
|
||||
$startIcon.remove();
|
||||
$this.prepend(spriteIcon('star'));
|
||||
$this.prepend(spriteIcon('star', 'icon'));
|
||||
}
|
||||
})
|
||||
.catch(() => Flash('Star toggle failed. Try again later.'));
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mr-widget-heading deploy-heading append-bottom-default">
|
||||
<div class="deploy-heading">
|
||||
<div class="ci-widget media">
|
||||
<div class="media-body">
|
||||
<div class="deploy-body">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<div class="mr-widget-heading">
|
||||
<div class="mr-widget-content"><slot name="default"></slot></div>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -6,6 +6,7 @@ import Icon from '~/vue_shared/components/icon.vue';
|
|||
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
|
||||
import MrWidgetIcon from './mr_widget_icon.vue';
|
||||
|
||||
export default {
|
||||
name: 'MRWidgetHeader',
|
||||
|
|
@ -13,6 +14,7 @@ export default {
|
|||
Icon,
|
||||
clipboardButton,
|
||||
TooltipOnTruncate,
|
||||
MrWidgetIcon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
|
|
@ -76,7 +78,7 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<div class="mr-source-target append-bottom-default">
|
||||
<div class="git-merge-icon-container append-right-default"><icon name="git-merge" /></div>
|
||||
<mr-widget-icon name="git-merge" />
|
||||
<div class="git-merge-container d-flex">
|
||||
<div class="normal">
|
||||
<strong>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
<script>
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: { Icon },
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="circle-icon-container append-right-default"><icon :name="name" /></div>
|
||||
</template>
|
||||
|
|
@ -79,67 +79,65 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasPipeline || hasCIError" class="mr-widget-heading append-bottom-default">
|
||||
<div class="ci-widget media">
|
||||
<template v-if="hasCIError">
|
||||
<div
|
||||
class="add-border ci-status-icon ci-status-icon-failed ci-error
|
||||
js-ci-error append-right-default"
|
||||
>
|
||||
<icon :size="32" name="status_failed_borderless" />
|
||||
</div>
|
||||
<div class="media-body" v-html="errorText"></div>
|
||||
</template>
|
||||
<template v-else-if="hasPipeline">
|
||||
<a :href="status.details_path" class="align-self-start append-right-default">
|
||||
<ci-icon :status="status" :size="32" :borderless="true" class="add-border" />
|
||||
</a>
|
||||
<div class="ci-widget-container d-flex">
|
||||
<div class="ci-widget-content">
|
||||
<div class="media-body">
|
||||
<div class="font-weight-bold">
|
||||
Pipeline
|
||||
<a :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number"
|
||||
>#{{ pipeline.id }}</a
|
||||
<div v-if="hasPipeline || hasCIError" class="ci-widget media">
|
||||
<template v-if="hasCIError">
|
||||
<div
|
||||
class="add-border ci-status-icon ci-status-icon-failed ci-error
|
||||
js-ci-error append-right-default"
|
||||
>
|
||||
<icon :size="32" name="status_failed_borderless" />
|
||||
</div>
|
||||
<div class="media-body" v-html="errorText"></div>
|
||||
</template>
|
||||
<template v-else-if="hasPipeline">
|
||||
<a :href="status.details_path" class="align-self-start append-right-default">
|
||||
<ci-icon :status="status" :size="32" :borderless="true" class="add-border" />
|
||||
</a>
|
||||
<div class="ci-widget-container d-flex">
|
||||
<div class="ci-widget-content">
|
||||
<div class="media-body">
|
||||
<div class="font-weight-bold">
|
||||
Pipeline
|
||||
<a :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number"
|
||||
>#{{ pipeline.id }}</a
|
||||
>
|
||||
|
||||
{{ pipeline.details.status.label }}
|
||||
|
||||
<template v-if="hasCommitInfo">
|
||||
for
|
||||
<a
|
||||
:href="pipeline.commit.commit_path"
|
||||
class="commit-sha js-commit-link font-weight-normal"
|
||||
>
|
||||
|
||||
{{ pipeline.details.status.label }}
|
||||
|
||||
<template v-if="hasCommitInfo">
|
||||
for
|
||||
<a
|
||||
:href="pipeline.commit.commit_path"
|
||||
class="commit-sha js-commit-link font-weight-normal"
|
||||
>
|
||||
{{ pipeline.commit.short_id }}</a
|
||||
>
|
||||
on
|
||||
<tooltip-on-truncate
|
||||
:title="sourceBranch"
|
||||
truncate-target="child"
|
||||
class="label-branch label-truncate"
|
||||
v-html="sourceBranchLink"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="pipeline.coverage" class="coverage">Coverage {{ pipeline.coverage }}%</div>
|
||||
{{ pipeline.commit.short_id }}</a
|
||||
>
|
||||
on
|
||||
<tooltip-on-truncate
|
||||
:title="sourceBranch"
|
||||
truncate-target="child"
|
||||
class="label-branch label-truncate"
|
||||
v-html="sourceBranchLink"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="mr-widget-pipeline-graph">
|
||||
<span v-if="hasStages" class="stage-cell">
|
||||
<div
|
||||
v-for="(stage, i) in pipeline.details.stages"
|
||||
:key="i"
|
||||
class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages"
|
||||
>
|
||||
<pipeline-stage :stage="stage" />
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
<div v-if="pipeline.coverage" class="coverage">Coverage {{ pipeline.coverage }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
<span class="mr-widget-pipeline-graph">
|
||||
<span v-if="hasStages" class="stage-cell">
|
||||
<div
|
||||
v-for="(stage, i) in pipeline.details.stages"
|
||||
:key="i"
|
||||
class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages"
|
||||
>
|
||||
<pipeline-stage :stage="stage" />
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
<script>
|
||||
import Deployment from './deployment.vue';
|
||||
import MrWidgetContainer from './mr_widget_container.vue';
|
||||
import MrWidgetPipeline from './mr_widget_pipeline.vue';
|
||||
|
||||
/**
|
||||
* Renders the pipeline and related deployments from the store.
|
||||
*
|
||||
* | Props | Description
|
||||
* |---------------|-------------
|
||||
* | `mr` | This is the mr_widget store
|
||||
* | `isPostMerge` | If true, show the "post merge" pipeline and deployments
|
||||
*/
|
||||
export default {
|
||||
name: 'MrWidgetPipelineContainer',
|
||||
components: {
|
||||
Deployment,
|
||||
MrWidgetContainer,
|
||||
MrWidgetPipeline,
|
||||
},
|
||||
props: {
|
||||
mr: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isPostMerge: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
pipeline() {
|
||||
return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline;
|
||||
},
|
||||
branch() {
|
||||
return this.isPostMerge ? this.mr.targetBranch : this.mr.sourceBranch;
|
||||
},
|
||||
branchLink() {
|
||||
return this.isPostMerge ? this.mr.targetBranch : this.mr.sourceBranchLink;
|
||||
},
|
||||
deployments() {
|
||||
return this.isPostMerge ? this.mr.postMergeDeployments : this.mr.deployments;
|
||||
},
|
||||
deploymentClass() {
|
||||
return this.isPostMerge ? 'js-post-deployment' : 'js-pre-deployment';
|
||||
},
|
||||
hasDeploymentMetrics() {
|
||||
return this.isPostMerge;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<mr-widget-container>
|
||||
<mr-widget-pipeline
|
||||
:pipeline="pipeline"
|
||||
:ci-status="mr.ciStatus"
|
||||
:has-ci="mr.hasCI"
|
||||
:source-branch="branch"
|
||||
:source-branch-link="branchLink"
|
||||
:troubleshooting-docs-path="mr.troubleshootingDocsPath"
|
||||
/>
|
||||
<div v-if="deployments.length" slot="footer" class="mr-widget-extension">
|
||||
<deployment
|
||||
v-for="deployment in deployments"
|
||||
:key="deployment.id"
|
||||
:class="deploymentClass"
|
||||
:deployment="deployment"
|
||||
:show-metrics="hasDeploymentMetrics"
|
||||
/>
|
||||
</div>
|
||||
</mr-widget-container>
|
||||
</template>
|
||||
|
|
@ -6,7 +6,7 @@ import SmartInterval from '~/smart_interval';
|
|||
import createFlash from '../flash';
|
||||
import WidgetHeader from './components/mr_widget_header.vue';
|
||||
import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
|
||||
import WidgetPipeline from './components/mr_widget_pipeline.vue';
|
||||
import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue';
|
||||
import Deployment from './components/deployment.vue';
|
||||
import WidgetRelatedLinks from './components/mr_widget_related_links.vue';
|
||||
import MergedState from './components/states/mr_widget_merged.vue';
|
||||
|
|
@ -44,7 +44,7 @@ export default {
|
|||
components: {
|
||||
'mr-widget-header': WidgetHeader,
|
||||
'mr-widget-merge-help': WidgetMergeHelp,
|
||||
'mr-widget-pipeline': WidgetPipeline,
|
||||
MrWidgetPipelineContainer,
|
||||
Deployment,
|
||||
'mr-widget-related-links': WidgetRelatedLinks,
|
||||
'mr-widget-merged': MergedState,
|
||||
|
|
@ -296,23 +296,12 @@ export default {
|
|||
<template>
|
||||
<div class="mr-state-widget prepend-top-default">
|
||||
<mr-widget-header :mr="mr" />
|
||||
<mr-widget-pipeline
|
||||
<mr-widget-pipeline-container
|
||||
v-if="shouldRenderPipelines"
|
||||
:pipeline="mr.pipeline"
|
||||
:ci-status="mr.ciStatus"
|
||||
:has-ci="mr.hasCI"
|
||||
:source-branch="mr.sourceBranch"
|
||||
:source-branch-link="mr.sourceBranchLink"
|
||||
:troubleshooting-docs-path="mr.troubleshootingDocsPath"
|
||||
class="mr-widget-workflow"
|
||||
:mr="mr"
|
||||
/>
|
||||
<deployment
|
||||
v-for="deployment in mr.deployments"
|
||||
:key="`pre-merge-deploy-${deployment.id}`"
|
||||
class="js-pre-merge-deploy"
|
||||
:deployment="deployment"
|
||||
:show-metrics="false"
|
||||
/>
|
||||
<div class="mr-section-container">
|
||||
<div class="mr-section-container mr-widget-workflow">
|
||||
<grouped-test-reports-app
|
||||
v-if="mr.testResultsPath"
|
||||
class="js-reports-container"
|
||||
|
|
@ -336,24 +325,11 @@ export default {
|
|||
</div>
|
||||
<div v-if="shouldRenderMergeHelp" class="mr-widget-footer"><mr-widget-merge-help /></div>
|
||||
</div>
|
||||
|
||||
<template v-if="shouldRenderMergedPipeline">
|
||||
<mr-widget-pipeline
|
||||
class="js-post-merge-pipeline prepend-top-default"
|
||||
:pipeline="mr.mergePipeline"
|
||||
:ci-status="mr.ciStatus"
|
||||
:has-ci="mr.hasCI"
|
||||
:source-branch="mr.targetBranch"
|
||||
:source-branch-link="mr.targetBranch"
|
||||
:troubleshooting-docs-path="mr.troubleshootingDocsPath"
|
||||
/>
|
||||
<deployment
|
||||
v-for="postMergeDeployment in mr.postMergeDeployments"
|
||||
:key="`post-merge-deploy-${postMergeDeployment.id}`"
|
||||
:deployment="postMergeDeployment"
|
||||
:show-metrics="true"
|
||||
class="js-post-deployment"
|
||||
/>
|
||||
</template>
|
||||
<mr-widget-pipeline-container
|
||||
v-if="shouldRenderMergedPipeline"
|
||||
class="js-post-merge-pipeline mr-widget-workflow"
|
||||
:mr="mr"
|
||||
:is-post-merge="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -17,12 +17,14 @@
|
|||
* />
|
||||
*/
|
||||
import { mapGetters } from 'vuex';
|
||||
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
||||
import userAvatarLink from '../user_avatar/user_avatar_link.vue';
|
||||
|
||||
export default {
|
||||
name: 'PlaceholderNote',
|
||||
components: {
|
||||
userAvatarLink,
|
||||
TimelineEntryItem,
|
||||
},
|
||||
props: {
|
||||
note: {
|
||||
|
|
@ -37,30 +39,28 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<li class="note being-posted fade-in-half timeline-entry">
|
||||
<div class="timeline-entry-inner">
|
||||
<div class="timeline-icon">
|
||||
<user-avatar-link
|
||||
:link-href="getUserData.path"
|
||||
:img-src="getUserData.avatar_url"
|
||||
:img-size="40"
|
||||
/>
|
||||
</div>
|
||||
<div :class="{ discussion: !note.individual_note }" class="timeline-content">
|
||||
<div class="note-header">
|
||||
<div class="note-header-info">
|
||||
<a :href="getUserData.path">
|
||||
<span class="d-none d-sm-inline-block">{{ getUserData.name }}</span>
|
||||
<span class="note-headline-light">@{{ getUserData.username }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<timeline-entry-item class="note being-posted fade-in-half">
|
||||
<div class="timeline-icon">
|
||||
<user-avatar-link
|
||||
:link-href="getUserData.path"
|
||||
:img-src="getUserData.avatar_url"
|
||||
:img-size="40"
|
||||
/>
|
||||
</div>
|
||||
<div :class="{ discussion: !note.individual_note }" class="timeline-content">
|
||||
<div class="note-header">
|
||||
<div class="note-header-info">
|
||||
<a :href="getUserData.path">
|
||||
<span class="d-none d-sm-inline-block">{{ getUserData.name }}</span>
|
||||
<span class="note-headline-light">@{{ getUserData.username }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="note-body">
|
||||
<div class="note-text">
|
||||
<p>{{ note.body }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="note-body">
|
||||
<div class="note-text">
|
||||
<p>{{ note.body }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</timeline-entry-item>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<script>
|
||||
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
||||
|
||||
/**
|
||||
* Common component to render a placeholder system note.
|
||||
*
|
||||
|
|
@ -9,6 +11,9 @@
|
|||
*/
|
||||
export default {
|
||||
name: 'PlaceholderSystemNote',
|
||||
components: {
|
||||
TimelineEntryItem,
|
||||
},
|
||||
props: {
|
||||
note: {
|
||||
type: Object,
|
||||
|
|
@ -19,11 +24,9 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<li class="note system-note timeline-entry being-posted fade-in-half">
|
||||
<div class="timeline-entry-inner">
|
||||
<div class="timeline-content">
|
||||
<em>{{ note.body }}</em>
|
||||
</div>
|
||||
<timeline-entry-item class="note system-note being-posted fade-in-half">
|
||||
<div class="timeline-content">
|
||||
<em>{{ note.body }}</em>
|
||||
</div>
|
||||
</li>
|
||||
</timeline-entry-item>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
<script>
|
||||
import { GlSkeletonLoading } from '@gitlab/ui';
|
||||
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
||||
|
||||
export default {
|
||||
name: 'SkeletonNote',
|
||||
components: {
|
||||
GlSkeletonLoading,
|
||||
TimelineEntryItem,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="timeline-entry note note-wrapper">
|
||||
<div class="timeline-entry-inner">
|
||||
<div class="timeline-icon"></div>
|
||||
<div class="timeline-content">
|
||||
<div class="note-header"></div>
|
||||
<div class="note-body"><gl-skeleton-loading /></div>
|
||||
</div>
|
||||
<timeline-entry-item class="note note-wrapper">
|
||||
<div class="timeline-icon"></div>
|
||||
<div class="timeline-content">
|
||||
<div class="note-header"></div>
|
||||
<div class="note-body"><gl-skeleton-loading /></div>
|
||||
</div>
|
||||
</li>
|
||||
</timeline-entry-item>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import $ from 'jquery';
|
|||
import { mapGetters } from 'vuex';
|
||||
import noteHeader from '~/notes/components/note_header.vue';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import TimelineEntryItem from './timeline_entry_item.vue';
|
||||
import { spriteIcon } from '../../../lib/utils/common_utils';
|
||||
|
||||
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
|
||||
|
|
@ -29,6 +30,7 @@ export default {
|
|||
components: {
|
||||
Icon,
|
||||
noteHeader,
|
||||
TimelineEntryItem,
|
||||
},
|
||||
props: {
|
||||
note: {
|
||||
|
|
@ -73,36 +75,34 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
<timeline-entry-item
|
||||
:id="noteAnchorId"
|
||||
:class="{ target: isTargetNote }"
|
||||
class="note system-note timeline-entry note-wrapper"
|
||||
class="note system-note note-wrapper"
|
||||
>
|
||||
<div class="timeline-entry-inner">
|
||||
<div class="timeline-icon" v-html="iconHtml"></div>
|
||||
<div class="timeline-content">
|
||||
<div class="note-header">
|
||||
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
|
||||
<span v-html="actionTextHtml"></span>
|
||||
</note-header>
|
||||
</div>
|
||||
<div class="note-body">
|
||||
<div
|
||||
:class="{
|
||||
'system-note-commit-list': hasMoreCommits,
|
||||
'hide-shade': expanded,
|
||||
}"
|
||||
class="note-text"
|
||||
v-html="note.note_html"
|
||||
></div>
|
||||
<div v-if="hasMoreCommits" class="flex-list">
|
||||
<div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded;">
|
||||
<icon :name="toggleIcon" :size="8" class="append-right-5" />
|
||||
<span>Toggle commit list</span>
|
||||
</div>
|
||||
<div class="timeline-icon" v-html="iconHtml"></div>
|
||||
<div class="timeline-content">
|
||||
<div class="note-header">
|
||||
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
|
||||
<span v-html="actionTextHtml"></span>
|
||||
</note-header>
|
||||
</div>
|
||||
<div class="note-body">
|
||||
<div
|
||||
:class="{
|
||||
'system-note-commit-list': hasMoreCommits,
|
||||
'hide-shade': expanded,
|
||||
}"
|
||||
class="note-text"
|
||||
v-html="note.note_html"
|
||||
></div>
|
||||
<div v-if="hasMoreCommits" class="flex-list">
|
||||
<div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded;">
|
||||
<icon :name="toggleIcon" :size="8" class="append-right-5" />
|
||||
<span>Toggle commit list</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</timeline-entry-item>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'TimelineEntryItem',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="timeline-entry">
|
||||
<div class="timeline-entry-inner"><slot></slot></div>
|
||||
</li>
|
||||
</template>
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
@import 'bootstrap_migration';
|
||||
@import 'framework/layout';
|
||||
|
||||
@import 'framework/alerts';
|
||||
@import 'framework/animations';
|
||||
@import 'framework/vue_transitions';
|
||||
@import 'framework/avatar';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
.alert-tip {
|
||||
background-color: $theme-gray-100;
|
||||
color: $theme-gray-900;
|
||||
}
|
||||
|
|
@ -33,7 +33,11 @@
|
|||
.bs-callout-warning {
|
||||
background-color: $orange-100;
|
||||
border-color: $orange-200;
|
||||
color: $orange-700;
|
||||
color: $orange-900;
|
||||
|
||||
a {
|
||||
color: $orange-900;
|
||||
}
|
||||
}
|
||||
|
||||
.bs-callout-info {
|
||||
|
|
|
|||
|
|
@ -363,6 +363,12 @@
|
|||
background-color: $white-light;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.filter-dropdown-container {
|
||||
.dropdown {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
|
|
@ -372,16 +378,6 @@
|
|||
.dropdown-menu {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.fa-chevron-down {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
padding: 10px;
|
||||
text-align: right;
|
||||
float: left;
|
||||
line-height: 1;
|
||||
|
||||
a {
|
||||
font-family: $monospace-font;
|
||||
|
|
|
|||
|
|
@ -80,3 +80,15 @@
|
|||
.user-avatar-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.circle-icon-container {
|
||||
$border-size: 1px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: $border-size solid $theme-gray-400;
|
||||
border-radius: 50%;
|
||||
padding: $gl-padding-8 - $border-size;
|
||||
color: $theme-gray-700;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,6 +158,10 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.dropdown-menu-toggle {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
form {
|
||||
display: block;
|
||||
height: auto;
|
||||
|
|
|
|||
|
|
@ -31,16 +31,6 @@
|
|||
|
||||
.timeline-entry-inner {
|
||||
position: relative;
|
||||
|
||||
@include notes-media('max', map-get($grid-breakpoints, sm)) {
|
||||
.timeline-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:target,
|
||||
|
|
|
|||
|
|
@ -243,6 +243,7 @@ $gl-padding-top: 10px;
|
|||
$gl-sidebar-padding: 22px;
|
||||
$gl-bar-padding: 3px;
|
||||
$input-horizontal-padding: 12px;
|
||||
$browserScrollbarSize: 10px;
|
||||
|
||||
/*
|
||||
* Misc
|
||||
|
|
|
|||
|
|
@ -50,9 +50,19 @@
|
|||
.mr-widget-heading {
|
||||
position: relative;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 4px;
|
||||
border-radius: $border-radius-default;
|
||||
}
|
||||
|
||||
&:not(.deploy-heading)::before {
|
||||
.mr-widget-extension {
|
||||
border-top: 1px solid $border-color;
|
||||
background-color: $gray-light;
|
||||
}
|
||||
|
||||
.mr-widget-workflow {
|
||||
margin-top: $gl-padding;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
border-left: 1px solid $theme-gray-200;
|
||||
position: absolute;
|
||||
|
|
@ -68,8 +78,8 @@
|
|||
border-top: 0;
|
||||
}
|
||||
|
||||
.mr-widget-heading,
|
||||
.mr-widget-section,
|
||||
.mr-widget-content,
|
||||
.mr-widget-footer {
|
||||
padding: $gl-padding;
|
||||
}
|
||||
|
|
@ -560,19 +570,6 @@
|
|||
color: $gl-text-color;
|
||||
}
|
||||
|
||||
.git-merge-icon-container {
|
||||
border: 1px solid $theme-gray-400;
|
||||
border-radius: 50%;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
color: $theme-gray-700;
|
||||
line-height: 28px;
|
||||
|
||||
.ic-git-merge {
|
||||
vertical-align: middle;
|
||||
width: 31px;
|
||||
}
|
||||
}
|
||||
|
||||
.git-merge-container {
|
||||
justify-content: space-between;
|
||||
|
|
@ -854,11 +851,6 @@
|
|||
}
|
||||
|
||||
.deploy-heading {
|
||||
margin-top: -19px;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
background-color: $gray-light;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding: $gl-padding-8 $gl-padding;
|
||||
}
|
||||
|
|
@ -868,6 +860,10 @@
|
|||
font-size: 12px;
|
||||
margin-left: 48px;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.deploy-body {
|
||||
|
|
|
|||
|
|
@ -589,12 +589,6 @@ $note-form-margin-left: 72px;
|
|||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.note-header-author-name {
|
||||
@include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.note-headline-light {
|
||||
display: inline;
|
||||
|
||||
|
|
|
|||
|
|
@ -723,7 +723,8 @@
|
|||
.scrolling-tabs-container {
|
||||
.scrolling-tabs {
|
||||
margin-top: $gl-padding-8;
|
||||
margin-bottom: $gl-padding-8;
|
||||
margin-bottom: $gl-padding-8 - $browserScrollbarSize;
|
||||
padding-bottom: $browserScrollbarSize;
|
||||
flex-wrap: wrap;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
|
@ -731,7 +732,7 @@
|
|||
.fade-left,
|
||||
.fade-right {
|
||||
top: 0;
|
||||
height: 100%;
|
||||
height: calc(100% - #{$browserScrollbarSize});
|
||||
|
||||
.fa {
|
||||
top: 50%;
|
||||
|
|
|
|||
|
|
@ -104,11 +104,23 @@
|
|||
border-bottom: 1px solid $white-normal;
|
||||
border-top: 1px solid $white-normal;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom-color: $white-light;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
th {
|
||||
border-top-color: $gray-light;
|
||||
}
|
||||
|
||||
td {
|
||||
border-color: $border-color;
|
||||
}
|
||||
|
||||
&:hover:not(.tree-truncated-warning) {
|
||||
td {
|
||||
background-color: $blue-50;
|
||||
|
|
|
|||
|
|
@ -5,23 +5,12 @@ class Admin::ImpersonationsController < Admin::ApplicationController
|
|||
before_action :authenticate_impersonator!
|
||||
|
||||
def destroy
|
||||
original_user = current_user
|
||||
|
||||
warden.set_user(impersonator, scope: :user)
|
||||
|
||||
Gitlab::AppLogger.info("User #{impersonator.username} has stopped impersonating #{original_user.username}")
|
||||
|
||||
session[:impersonator_id] = nil
|
||||
|
||||
original_user = stop_impersonation
|
||||
redirect_to admin_user_path(original_user), status: :found
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def impersonator
|
||||
@impersonator ||= User.find(session[:impersonator_id]) if session[:impersonator_id]
|
||||
end
|
||||
|
||||
def authenticate_impersonator!
|
||||
render_404 unless impersonator && impersonator.admin? && !impersonator.blocked?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class Admin::RequestsProfilesController < Admin::ApplicationController
|
|||
profile = Gitlab::RequestProfiler::Profile.find(clean_name)
|
||||
|
||||
if profile
|
||||
render text: profile.content
|
||||
render html: profile.content
|
||||
else
|
||||
redirect_to admin_requests_profiles_path, alert: 'Profile not found'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class Admin::UsersController < Admin::ApplicationController
|
||||
before_action :user, except: [:index, :new, :create]
|
||||
before_action :check_impersonation_availability, only: :impersonate
|
||||
|
||||
def index
|
||||
@users = User.order_name_asc.filter(params[:filter])
|
||||
|
|
@ -227,4 +228,8 @@ class Admin::UsersController < Admin::ApplicationController
|
|||
|
||||
result[:status] == :success
|
||||
end
|
||||
|
||||
def check_impersonation_availability
|
||||
access_denied! unless Gitlab.config.gitlab.impersonation_enabled
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ class ApplicationController < ActionController::Base
|
|||
before_action :configure_permitted_parameters, if: :devise_controller?
|
||||
before_action :require_email, unless: :devise_controller?
|
||||
before_action :set_usage_stats_consent_flag
|
||||
before_action :check_impersonation_availability
|
||||
|
||||
around_action :set_locale
|
||||
|
||||
|
|
@ -462,4 +463,28 @@ class ApplicationController < ActionController::Base
|
|||
.new(settings, current_user, application_setting_params)
|
||||
.execute
|
||||
end
|
||||
|
||||
def check_impersonation_availability
|
||||
return unless session[:impersonator_id]
|
||||
|
||||
unless Gitlab.config.gitlab.impersonation_enabled
|
||||
stop_impersonation
|
||||
access_denied! _('Impersonation has been disabled')
|
||||
end
|
||||
end
|
||||
|
||||
def stop_impersonation
|
||||
impersonated_user = current_user
|
||||
|
||||
Gitlab::AppLogger.info("User #{impersonator.username} has stopped impersonating #{impersonated_user.username}")
|
||||
|
||||
warden.set_user(impersonator, scope: :user)
|
||||
session[:impersonator_id] = nil
|
||||
|
||||
impersonated_user
|
||||
end
|
||||
|
||||
def impersonator
|
||||
@impersonator ||= User.find(session[:impersonator_id]) if session[:impersonator_id]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class ChaosController < ActionController::Base
|
|||
duration_taken = (Time.now - start).seconds
|
||||
Kernel.sleep duration_s - duration_taken if duration_s > duration_taken
|
||||
|
||||
render text: "OK", content_type: 'text/plain'
|
||||
render plain: "OK"
|
||||
end
|
||||
|
||||
def cpuspin
|
||||
|
|
@ -24,14 +24,14 @@ class ChaosController < ActionController::Base
|
|||
|
||||
rand while Time.now < end_time
|
||||
|
||||
render text: "OK", content_type: 'text/plain'
|
||||
render plain: "OK"
|
||||
end
|
||||
|
||||
def sleep
|
||||
duration_s = (params[:duration_s]&.to_i || 30).seconds
|
||||
Kernel.sleep duration_s
|
||||
|
||||
render text: "OK", content_type: 'text/plain'
|
||||
render plain: "OK"
|
||||
end
|
||||
|
||||
def kill
|
||||
|
|
@ -44,13 +44,13 @@ class ChaosController < ActionController::Base
|
|||
secret = ENV['GITLAB_CHAOS_SECRET']
|
||||
# GITLAB_CHAOS_SECRET is required unless you're running in Development mode
|
||||
if !secret && !Rails.env.development?
|
||||
render text: "chaos misconfigured: please configure GITLAB_CHAOS_SECRET when using GITLAB_ENABLE_CHAOS_ENDPOINTS outside of a development environment", content_type: 'text/plain', status: 500
|
||||
render plain: "chaos misconfigured: please configure GITLAB_CHAOS_SECRET when using GITLAB_ENABLE_CHAOS_ENDPOINTS outside of a development environment", status: :internal_server_error
|
||||
end
|
||||
|
||||
return unless secret
|
||||
|
||||
unless request.headers["HTTP_X_CHAOS_SECRET"] == secret
|
||||
render text: "To experience chaos, please set X-Chaos-Secret header", content_type: 'text/plain', status: 401
|
||||
render plain: "To experience chaos, please set X-Chaos-Secret header", status: :unauthorized
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class MetricsController < ActionController::Base
|
|||
"# Metrics are disabled, see: #{help_page}\n"
|
||||
end
|
||||
|
||||
render text: response, content_type: 'text/plain; version=0.0.4'
|
||||
render plain: response, content_type: 'text/plain; version=0.0.4'
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -41,12 +41,12 @@ class Profiles::KeysController < Profiles::ApplicationController
|
|||
user = UserFinder.new(params[:username]).find_by_username
|
||||
if user.present?
|
||||
headers['Content-Disposition'] = 'attachment'
|
||||
render text: user.all_ssh_keys.join("\n"), content_type: 'text/plain'
|
||||
render plain: user.all_ssh_keys.join("\n")
|
||||
else
|
||||
return render_404
|
||||
end
|
||||
rescue => e
|
||||
render text: e.message
|
||||
render html: e.message
|
||||
end
|
||||
else
|
||||
return render_404
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
|||
set_workhorse_internal_api_content_type
|
||||
render json: Gitlab::Workhorse.terminal_websocket(terminal)
|
||||
else
|
||||
render text: 'Not found', status: :not_found
|
||||
render html: 'Not found', status: :not_found
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ module UsersHelper
|
|||
end
|
||||
end
|
||||
|
||||
def impersonation_enabled?
|
||||
Gitlab.config.gitlab.impersonation_enabled
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_profile_tabs
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ module Ci
|
|||
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
|
||||
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :variables, class_name: 'Ci::PipelineVariable'
|
||||
has_many :deployments, through: :builds
|
||||
has_many :environments, -> { distinct }, through: :deployments
|
||||
|
||||
# Merge requests for which the current pipeline is running against
|
||||
# the merge request's latest commit.
|
||||
|
|
@ -523,10 +525,6 @@ module Ci
|
|||
yaml_errors.present?
|
||||
end
|
||||
|
||||
def environments
|
||||
builds.where.not(environment: nil).success.pluck(:environment).uniq
|
||||
end
|
||||
|
||||
# Manually set the notes for a Ci::Pipeline
|
||||
# There is no ActiveRecord relation between Ci::Pipeline and notes
|
||||
# as they are related to a commit sha. This method helps importing
|
||||
|
|
|
|||
|
|
@ -56,7 +56,11 @@ module Clusters
|
|||
def specification
|
||||
{
|
||||
"ingress" => {
|
||||
"hosts" => [hostname]
|
||||
"hosts" => [hostname],
|
||||
"tls" => [{
|
||||
"hosts" => [hostname],
|
||||
"secretName" => "jupyter-cert"
|
||||
}]
|
||||
},
|
||||
"hub" => {
|
||||
"extraEnv" => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Shardable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
belongs_to :shard
|
||||
validates :shard, presence: true
|
||||
end
|
||||
|
||||
def shard_name
|
||||
shard&.name
|
||||
end
|
||||
|
||||
def shard_name=(name)
|
||||
self.shard = Shard.by_name(name)
|
||||
end
|
||||
end
|
||||
|
|
@ -12,13 +12,13 @@ class EnvironmentStatus
|
|||
delegate :deployed_at, to: :deployment, allow_nil: true
|
||||
|
||||
def self.for_merge_request(mr, user)
|
||||
build_environments_status(mr, user, mr.diff_head_sha)
|
||||
build_environments_status(mr, user, mr.actual_head_pipeline)
|
||||
end
|
||||
|
||||
def self.after_merge_request(mr, user)
|
||||
return [] unless mr.merged?
|
||||
|
||||
build_environments_status(mr, user, mr.merge_commit_sha)
|
||||
build_environments_status(mr, user, mr.merge_pipeline)
|
||||
end
|
||||
|
||||
def initialize(environment, merge_request, sha)
|
||||
|
|
@ -61,13 +61,13 @@ class EnvironmentStatus
|
|||
}
|
||||
end
|
||||
|
||||
def self.build_environments_status(mr, user, sha)
|
||||
Environment.where(project_id: [mr.source_project_id, mr.target_project_id])
|
||||
.available
|
||||
.with_deployment(sha).map do |environment|
|
||||
def self.build_environments_status(mr, user, pipeline)
|
||||
return [] unless pipeline
|
||||
|
||||
pipeline.environments.available.map do |environment|
|
||||
next unless Ability.allowed?(user, :read_environment, environment)
|
||||
|
||||
EnvironmentStatus.new(environment, mr, sha)
|
||||
EnvironmentStatus.new(environment, mr, pipeline.sha)
|
||||
end.compact
|
||||
end
|
||||
private_class_method :build_environments_status
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ class NotificationSetting < ActiveRecord::Base
|
|||
|
||||
ignore_column :events
|
||||
|
||||
enum level: { global: 3, watch: 2, mention: 4, participating: 1, disabled: 0, custom: 5 }
|
||||
enum level: { global: 3, watch: 2, participating: 1, mention: 4, disabled: 0, custom: 5 }
|
||||
|
||||
default_value_for :level, NotificationSetting.levels[:global]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PoolRepository < ActiveRecord::Base
|
||||
belongs_to :shard
|
||||
validates :shard, presence: true
|
||||
include Shardable
|
||||
|
||||
has_many :member_projects, class_name: 'Project'
|
||||
|
||||
after_create :correct_disk_path
|
||||
|
||||
def shard_name
|
||||
shard&.name
|
||||
end
|
||||
|
||||
def shard_name=(name)
|
||||
self.shard = Shard.by_name(name)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def correct_disk_path
|
||||
|
|
|
|||
|
|
@ -186,6 +186,7 @@ class Project < ActiveRecord::Base
|
|||
|
||||
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
|
||||
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_one :project_repository, inverse_of: :project
|
||||
|
||||
# Merge Requests for target project should be removed with it
|
||||
has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
|
||||
|
|
@ -1206,6 +1207,13 @@ class Project < ActiveRecord::Base
|
|||
false
|
||||
end
|
||||
|
||||
def track_project_repository
|
||||
return unless hashed_storage?(:repository)
|
||||
|
||||
project_repo = project_repository || build_project_repository
|
||||
project_repo.update!(shard_name: repository_storage, disk_path: disk_path)
|
||||
end
|
||||
|
||||
def create_repository(force: false)
|
||||
# Forked import is handled asynchronously
|
||||
return if forked? && !force
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProjectRepository < ActiveRecord::Base
|
||||
include Shardable
|
||||
|
||||
belongs_to :project, inverse_of: :project_repository
|
||||
|
||||
class << self
|
||||
def find_project(disk_path)
|
||||
find_by(disk_path: disk_path)&.project
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -15,8 +15,6 @@ class RemoteMirror < ActiveRecord::Base
|
|||
insecure_mode: true,
|
||||
algorithm: 'aes-256-cbc'
|
||||
|
||||
default_value_for :only_protected_branches, true
|
||||
|
||||
belongs_to :project, inverse_of: :remote_mirrors
|
||||
|
||||
validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true, enforce_user: true }
|
||||
|
|
|
|||
|
|
@ -85,6 +85,12 @@ class WikiPage
|
|||
|
||||
alias_method :to_param, :slug
|
||||
|
||||
def human_title
|
||||
return 'Home' if title == 'home'
|
||||
|
||||
title
|
||||
end
|
||||
|
||||
# The formatted title of this page.
|
||||
def title
|
||||
if @attributes[:title]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ class AccessTokenValidationService
|
|||
EXPIRED = :expired
|
||||
REVOKED = :revoked
|
||||
INSUFFICIENT_SCOPE = :insufficient_scope
|
||||
IMPERSONATION_DISABLED = :impersonation_disabled
|
||||
|
||||
attr_reader :token, :request
|
||||
|
||||
|
|
@ -24,6 +25,11 @@ class AccessTokenValidationService
|
|||
elsif !self.include_any_scope?(scopes)
|
||||
return INSUFFICIENT_SCOPE
|
||||
|
||||
elsif token.respond_to?(:impersonation) &&
|
||||
token.impersonation &&
|
||||
!Gitlab.config.gitlab.impersonation_enabled
|
||||
return IMPERSONATION_DISABLED
|
||||
|
||||
else
|
||||
return VALID
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
class ArchiveTraceService
|
||||
def execute(job)
|
||||
job.trace.archive!
|
||||
rescue ::Gitlab::Ci::Trace::AlreadyArchivedError
|
||||
# It's already archived, thus we can safely ignore this exception.
|
||||
rescue => e
|
||||
# Tracks this error with application logs, Sentry, and Prometheus.
|
||||
# If `archive!` keeps failing for over a week, that could incur data loss.
|
||||
# (See more https://docs.gitlab.com/ee/administration/job_traces.html#new-live-trace-architecture)
|
||||
# In order to avoid interrupting the system, we do not raise an exception here.
|
||||
archive_error(e, job)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def failed_archive_counter
|
||||
@failed_archive_counter ||=
|
||||
Gitlab::Metrics.counter(:job_trace_archive_failed_total,
|
||||
"Counter of failed attempts of trace archiving")
|
||||
end
|
||||
|
||||
def archive_error(error, job)
|
||||
failed_archive_counter.increment
|
||||
Rails.logger.error "Failed to archive trace. id: #{job.id} message: #{error.message}"
|
||||
|
||||
Gitlab::Sentry
|
||||
.track_exception(error,
|
||||
issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/51502',
|
||||
extra: { job_id: job.id })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -4,6 +4,8 @@ module Ci
|
|||
class CreatePipelineService < BaseService
|
||||
attr_reader :pipeline
|
||||
|
||||
CreateError = Class.new(StandardError)
|
||||
|
||||
SEQUENCE = [Gitlab::Ci::Pipeline::Chain::Build,
|
||||
Gitlab::Ci::Pipeline::Chain::Validate::Abilities,
|
||||
Gitlab::Ci::Pipeline::Chain::Validate::Repository,
|
||||
|
|
@ -47,6 +49,14 @@ module Ci
|
|||
pipeline
|
||||
end
|
||||
|
||||
def execute!(*args, &block)
|
||||
execute(*args, &block).tap do |pipeline|
|
||||
unless pipeline.persisted?
|
||||
raise CreateError, pipeline.errors.full_messages.join(',')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def commit
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ module Files
|
|||
transformer = Lfs::FileTransformer.new(project, @branch_name)
|
||||
|
||||
actions = actions_after_lfs_transformation(transformer, params[:actions])
|
||||
actions = transform_move_actions(actions)
|
||||
|
||||
commit_actions!(actions)
|
||||
end
|
||||
|
|
@ -26,6 +27,16 @@ module Files
|
|||
end
|
||||
end
|
||||
|
||||
# When moving a file, `content: nil` means "use the contents of the previous
|
||||
# file", while `content: ''` means "move the file and set it to empty"
|
||||
def transform_move_actions(actions)
|
||||
actions.map do |action|
|
||||
action[:infer_content] = true if action[:content].nil?
|
||||
|
||||
action
|
||||
end
|
||||
end
|
||||
|
||||
def commit_actions!(actions)
|
||||
repository.multi_action(
|
||||
current_user,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ module Projects
|
|||
end
|
||||
|
||||
def execute
|
||||
if @params[:template_name]&.present?
|
||||
if @params[:template_name].present?
|
||||
return ::Projects::CreateFromTemplateService.new(current_user, params).execute
|
||||
end
|
||||
|
||||
|
|
@ -86,6 +86,8 @@ module Projects
|
|||
@project.create_wiki unless skip_wiki?
|
||||
end
|
||||
|
||||
@project.track_project_repository
|
||||
|
||||
event_service.create_project(@project, current_user)
|
||||
system_hook_service.execute_hooks_for(@project, :create)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ module Projects
|
|||
|
||||
if result
|
||||
project.write_repository_config
|
||||
project.track_project_repository
|
||||
else
|
||||
rollback_folder_move
|
||||
project.storage_version = nil
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
- sorted_by = sort_options_hash[@sort]
|
||||
|
||||
.dropdown.inline.prepend-left-10
|
||||
%button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
|
||||
%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
|
||||
= sorted_by
|
||||
= icon('chevron-down')
|
||||
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
%span.cred (Admin)
|
||||
|
||||
.float-right
|
||||
- if @user != current_user && @user.can?(:log_in)
|
||||
- if impersonation_enabled? && @user != current_user && @user.can?(:log_in)
|
||||
= link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-nr btn-grouped btn-info"
|
||||
= link_to edit_admin_user_path(@user), class: "btn btn-nr btn-grouped" do
|
||||
%i.fa.fa-pencil-square-o
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
- if current_user
|
||||
.dropdown
|
||||
%button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown", 'data-display' => 'static' }
|
||||
= icon('globe')
|
||||
%span.light= _("Visibility:")
|
||||
%button.dropdown-menu-toggle{ href: '#', "data-toggle" => "dropdown", 'data-display' => 'static' }
|
||||
= icon('globe', class: 'mt-1')
|
||||
%span.light.ml-3= _("Visibility:")
|
||||
- if params[:visibility_level].present?
|
||||
= visibility_level_label(params[:visibility_level].to_i)
|
||||
- else
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
.settings-content
|
||||
= render 'shared/badges/badge_settings'
|
||||
|
||||
= render_if_exists 'groups/custom_project_templates_setting'
|
||||
= render_if_exists 'groups/templates_setting', expanded: expanded
|
||||
|
||||
%section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
- page_title "Invitation"
|
||||
%h3.page-title Invitation
|
||||
- page_title _("Invitation")
|
||||
%h3.page-title= _("Invitation")
|
||||
|
||||
%p
|
||||
You have been invited
|
||||
|
|
@ -24,14 +24,17 @@
|
|||
|
||||
- if is_member
|
||||
%p
|
||||
However, you are already a member of this #{@member.source.is_a?(Group) ? "group" : "project"}.
|
||||
Sign in using a different account to accept the invitation.
|
||||
- member_source = @member.source.is_a?(Group) ? _("group") : _("project")
|
||||
= _("However, you are already a member of this %{member_source}. Sign in using a different account to accept the invitation.") % { member_source: member_source }
|
||||
|
||||
- if @member.invite_email != current_user.email
|
||||
%p
|
||||
Note that this invitation was sent to #{mail_to @member.invite_email}, but you are signed in as #{link_to current_user.to_reference, user_url(current_user)} with email #{mail_to current_user.email}.
|
||||
- mail_to_invite_email = mail_to(@member.invite_email)
|
||||
- mail_to_current_user = mail_to(current_user.email)
|
||||
- link_to_current_user = link_to(current_user.to_reference, user_url(current_user))
|
||||
= _("Note that this invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}.").html_safe % { mail_to_invite_email: mail_to_invite_email, mail_to_current_user: mail_to_current_user, link_to_current_user: link_to_current_user }
|
||||
|
||||
- unless is_member
|
||||
.actions
|
||||
= link_to "Accept invitation", accept_invite_url(@token), method: :post, class: "btn btn-success"
|
||||
= link_to "Decline", decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10"
|
||||
= link_to _("Accept invitation"), accept_invite_url(@token), method: :post, class: "btn btn-success"
|
||||
= link_to _("Decline"), decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' }
|
||||
|
||||
.dropdown
|
||||
%button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
|
||||
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
|
||||
%span.light sort:
|
||||
- if @sort.present?
|
||||
= sort_options_hash[@sort]
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
= link_to icon('question-circle'), help_page_path('user/project/protected_branches')
|
||||
|
||||
.panel-footer
|
||||
= f.submit _('Mirror repository'), class: 'btn btn-success', name: :update_remote_mirror
|
||||
= f.submit _('Mirror repository'), class: 'btn btn-success js-mirror-submit', name: :update_remote_mirror
|
||||
|
||||
.panel.panel-default
|
||||
.table-responsive
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@
|
|||
.project-template
|
||||
.form-group
|
||||
%div
|
||||
= render 'project_templates', f: f
|
||||
= render 'project_templates', f: f, project: @project
|
||||
|
||||
.tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' }
|
||||
- if import_sources_enabled?
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@
|
|||
.text-muted
|
||||
= template.description
|
||||
.controls.d-flex.align-items-center
|
||||
%label.btn.btn-success.template-button.choose-template.append-right-10.append-bottom-0{ for: template.name }
|
||||
%a.btn.btn-default.append-right-10{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } }
|
||||
= _("Preview")
|
||||
%label.btn.btn-success.template-button.choose-template.append-bottom-0{ for: template.name }
|
||||
%input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } }
|
||||
%span
|
||||
= _("Use template")
|
||||
%a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } }
|
||||
= _("Preview")
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
= search_field_tag :search, params[:search], { placeholder: s_('TagsPage|Filter by tag name'), id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }
|
||||
|
||||
.dropdown
|
||||
%button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown'} }
|
||||
%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown'} }
|
||||
%span.light
|
||||
= tags_sort_options_hash[@sort]
|
||||
= icon('chevron-down')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
.tree-content-holder.js-tree-content{ 'data-logs-path': @logs_path }
|
||||
.table-holder
|
||||
.table-holder.bordered-box
|
||||
%table.table#tree-slider{ class: "table_#{@hex_path} tree-table qa-file-tree" }
|
||||
%thead
|
||||
%tr
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
%li{ class: active_when(params[:id] == wiki_page.slug) }
|
||||
= link_to project_wiki_path(@project, wiki_page) do
|
||||
= wiki_page.title.capitalize
|
||||
= wiki_page.human_title
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
- @content_class = "limit-container-width" unless fluid_layout
|
||||
- add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, @page)
|
||||
- breadcrumb_title @page.persisted? ? _("Edit") : _("New")
|
||||
- page_title @page.persisted? ? _("Edit") : _("New"), @page.title.capitalize, _("Wiki")
|
||||
- page_title @page.persisted? ? _("Edit") : _("New"), @page.human_title, _("Wiki")
|
||||
|
||||
= wiki_page_errors(@error)
|
||||
|
||||
|
|
@ -12,9 +12,9 @@
|
|||
.nav-text
|
||||
%h2.wiki-page-title
|
||||
- if @page.persisted?
|
||||
= link_to @page.title.capitalize, project_wiki_path(@project, @page)
|
||||
= link_to @page.human_title, project_wiki_path(@project, @page)
|
||||
- else
|
||||
= @page.title.capitalize
|
||||
= @page.human_title
|
||||
%span.light
|
||||
·
|
||||
- if @page.persisted?
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
= link_to project_wiki_history_path(@project, @page), class: "btn" do
|
||||
= s_("Wiki|Page history")
|
||||
- if can?(current_user, :admin_wiki, @project)
|
||||
#delete-wiki-modal-wrapper{ data: { delete_wiki_url: project_wiki_path(@project, @page), page_title: @page.title.capitalize } }
|
||||
#delete-wiki-modal-wrapper{ data: { delete_wiki_url: project_wiki_path(@project, @page), page_title: @page.human_title } }
|
||||
|
||||
= render 'form', uploads_path: wiki_attachment_upload_url
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
- page_title _("History"), @page.title.capitalize, _("Wiki")
|
||||
- page_title _("History"), @page.human_title, _("Wiki")
|
||||
|
||||
.wiki-page-header.has-sidebar-toggle
|
||||
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
.nav-text
|
||||
%h2.wiki-page-title
|
||||
= link_to @page.title.capitalize, project_wiki_path(@project, @page)
|
||||
= link_to @page.human_title, project_wiki_path(@project, @page)
|
||||
%span.light
|
||||
·
|
||||
= _("History")
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue