Merge commit '83f0798e7dc588f0e4cb6816daadeef7dbfc8b81' into fix/gb/encrypt-runners-tokens

* commit '83f0798e7dc588f0e4cb6816daadeef7dbfc8b81': (101 commits)
This commit is contained in:
Grzegorz Bizon 2018-12-04 09:11:33 +01:00
commit 6b4926f2aa
357 changed files with 4380 additions and 2072 deletions

View File

@ -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}"

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -1 +1 @@
1.1.0
1.2.0

View File

@ -1 +1 @@
7.2.1
7.3.0

View File

@ -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'

View File

@ -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)

View File

@ -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)

View File

@ -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',

View File

@ -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()

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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();
}

View File

@ -26,4 +26,5 @@ export default () => ({
currentDiffFileId: '',
projectPath: '',
commentForms: [],
highlightedRow: null,
});

View File

@ -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';

View File

@ -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;
},
};

View File

@ -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',

View File

@ -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) {

View File

@ -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">

View File

@ -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) {

View File

@ -30,6 +30,7 @@ export default class MirrorRepos {
this.$password.on('input.updateUrl', () => this.debouncedUpdateUrl());
this.initMirrorSSH();
this.updateProtectedBranches();
}
initMirrorSSH() {

View File

@ -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"

View File

@ -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 {

View File

@ -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>

View File

@ -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 }}

View File

@ -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>

View File

@ -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>

View File

@ -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;
})

View File

@ -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();
});

View File

@ -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');

View File

@ -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');
},

View File

@ -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"

View File

@ -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.'));

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -6,6 +6,7 @@
@import 'bootstrap_migration';
@import 'framework/layout';
@import 'framework/alerts';
@import 'framework/animations';
@import 'framework/vue_transitions';
@import 'framework/avatar';

View File

@ -0,0 +1,4 @@
.alert-tip {
background-color: $theme-gray-100;
color: $theme-gray-900;
}

View File

@ -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 {

View File

@ -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;
}
}
}

View File

@ -42,6 +42,7 @@
padding: 10px;
text-align: right;
float: left;
line-height: 1;
a {
font-family: $monospace-font;

View File

@ -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;
}

View File

@ -158,6 +158,10 @@
width: 100%;
}
.dropdown-menu-toggle {
margin-bottom: 0;
}
form {
display: block;
height: auto;

View File

@ -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,

View File

@ -243,6 +243,7 @@ $gl-padding-top: 10px;
$gl-sidebar-padding: 22px;
$gl-bar-padding: 3px;
$input-horizontal-padding: 12px;
$browserScrollbarSize: 10px;
/*
* Misc

View File

@ -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 {

View File

@ -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;

View File

@ -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%;

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -70,6 +70,10 @@ module UsersHelper
end
end
def impersonation_enabled?
Gitlab.config.gitlab.impersonation_enabled
end
private
def get_profile_tabs

View File

@ -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

View File

@ -56,7 +56,11 @@ module Clusters
def specification
{
"ingress" => {
"hosts" => [hostname]
"hosts" => [hostname],
"tls" => [{
"hosts" => [hostname],
"secretName" => "jupyter-cert"
}]
},
"hub" => {
"extraEnv" => {

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -30,6 +30,7 @@ module Projects
if result
project.write_repository_config
project.track_project_repository
else
rollback_folder_move
project.storage_version = nil

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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"

View File

@ -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]

View File

@ -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

View File

@ -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?

View File

@ -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")

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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
&middot;
- 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

View File

@ -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
&middot;
= _("History")

Some files were not shown because too many files have changed in this diff Show More