Commit caf33ea0 authored by Achilleas Pipinellis's avatar Achilleas Pipinellis

Merge remote-tracking branch 'origin/master' into tatkins-installation-method-docs

parents a2accebb 5ea6b08e
......@@ -35,4 +35,10 @@ if (BABEL_ENV === 'karma' || BABEL_ENV === 'coverage') {
plugins.push('babel-plugin-rewire');
}
// Jest is running in node environment
if (BABEL_ENV === 'jest') {
plugins.push('transform-es2015-modules-commonjs');
plugins.push('dynamic-import-node');
}
module.exports = { presets, plugins };
......@@ -2,6 +2,7 @@
/config/
/builds/
/coverage/
/coverage-frontend/
/coverage-javascript/
/node_modules/
/public/
......
......@@ -78,5 +78,5 @@ eslint-report.html
/plugins/*
/.gitlab_pages_secret
package-lock.json
/junit_rspec.xml
/junit_karma.xml
/junit_*.xml
/coverage-frontend/
......@@ -48,6 +48,7 @@ after_script:
stages:
- build
- prepare
- merge
- test
- post-test
- pages
......@@ -666,7 +667,7 @@ gitlab:assets:compile:
only:
- //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee
- //@gitlab/gitabhq
- //@gitlab/gitlabhq
- //@gitlab/gitlab-ee
tags:
- gitlab-org-delivery
......@@ -698,6 +699,32 @@ karma:
reports:
junit: junit_karma.xml
jest:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg
dependencies:
- compile-assets
- setup-test-env
script:
- scripts/gitaly-test-spawn
- date
- bundle exec rake karma:fixtures
- date
- yarn jest --ci --coverage
artifacts:
name: coverage-frontend
expire_in: 31d
when: always
paths:
- coverage-frontend/
- junit_jest.xml
reports:
junit: junit_jest.xml
cache:
key: jest
paths:
- tmp/jest/jest/
code_quality:
<<: *dedicated-no-docs-no-db-pull-cache-job
image: docker:stable
......@@ -978,9 +1005,10 @@ review-deploy:
review-qa-smoke:
<<: *review-qa-base
retry: 2
# retry: 2
script:
- gitlab-qa Test::Instance::Smoke "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
allow_failure: true
review-qa-all:
<<: *review-qa-base
......
......@@ -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
......
......@@ -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.
......@@ -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'
......@@ -298,7 +298,7 @@ gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.3'
gem 'gettext', '~> 3.2.2', require: false, group: :development
gem 'batch-loader', '~> 1.2.1'
gem 'batch-loader', '~> 1.2.2'
# Perf bar
gem 'peek', '~> 1.0.1'
......
......@@ -73,7 +73,7 @@ GEM
thread_safe (~> 0.3, >= 0.3.1)
babosa (1.0.2)
base32 (0.3.2)
batch-loader (1.2.1)
batch-loader (1.2.2)
bcrypt (3.1.12)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
......@@ -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
......@@ -950,7 +950,7 @@ DEPENDENCIES
awesome_print
babosa (~> 1.0.2)
base32 (~> 0.3.0)
batch-loader (~> 1.2.1)
batch-loader (~> 1.2.2)
bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0)
better_errors (~> 2.5.0)
......@@ -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)
......
......@@ -70,7 +70,7 @@ GEM
thread_safe (~> 0.3, >= 0.3.1)
babosa (1.0.2)
base32 (0.3.2)
batch-loader (1.2.1)
batch-loader (1.2.2)
bcrypt (3.1.12)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
......@@ -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
......@@ -941,7 +941,7 @@ DEPENDENCIES
awesome_print
babosa (~> 1.0.2)
base32 (~> 0.3.0)
batch-loader (~> 1.2.1)
batch-loader (~> 1.2.2)
bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0)
better_errors (~> 2.5.0)
......@@ -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',
......
......@@ -411,11 +411,10 @@ export default {
<div slot="description">
<p>
{{
s__(`ClusterIntegration|Knative (pronounced kay-nay-tiv) extends
Kubernetes to provide a set of middleware components that are
essential to build modern, source-centric, and container-based
applications that can run anywhere: on premises, in the cloud, or
even in a third-party data center.`)
s__(`ClusterIntegration|Knative extends Kubernetes to provide
a set of middleware components that are essential to build modern,
source-centric, and container-based applications that can run
anywhere: on premises, in the cloud, or even in a third-party data center.`)
}}
</p>
......
......@@ -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()
......
......@@ -56,9 +56,12 @@ export default {
return `${noteData.author.name}: ${note}`;
},
toggleDiscussions() {
const forceExpanded = this.discussions.some(discussion => !discussion.expanded);
this.discussions.forEach(discussion => {
this.toggleDiscussion({
discussionId: discussion.id,
forceExpanded,
});
});
},
......
......@@ -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>
......
<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>
......
<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>
<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,14 +141,14 @@ 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();
}
};
export const loadCollapsedDiff = ({ commit }, file) =>
axios.get(file.loadCollapsedDiffUrl).then(res => {
axios.get(file.load_collapsed_diff_url).then(res => {
commit(types.ADD_COLLAPSED_DIFFS, {
file,
data: res.data,
......
......@@ -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}=).*?(&|$)`);
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));
const re = /^([^?#]*)(\?[^#]*)?(.*)/;
const merged = {};
const urlparts = url.match(re);
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('='));
}
});
}
// Remove a trailing ampersand
const lastChar = newUrl[newUrl.length - 1];
Object.assign(merged, params);
if (lastChar === '&') {
newUrl = newUrl.slice(0, -1);
}
const query = Object.keys(merged)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(merged[key])}`)
.join('&');
return newUrl;
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 {
......
......@@ -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>
......@@ -178,9 +178,11 @@ export default {
}
},
[types.TOGGLE_DISCUSSION](state, { discussionId }) {
[types.TOGGLE_DISCUSSION](state, { discussionId, forceExpanded = null }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
Object.assign(discussion, { expanded: !discussion.expanded });
Object.assign(discussion, {
expanded: forceExpanded === null ? !discussion.expanded : forceExpanded,
});
},
[types.UPDATE_NOTE](state, note) {
......
......@@ -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();
});
......@@ -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">
......
<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>
......
<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 }}
{{ 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>
<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>
</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>
</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>
</div>
</span>
</div>
</template>
</div>
</div>
</template>
</div>
</template>
<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>
<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>
<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>
<script>
export default {
name: 'TimelineEntryItem',
};
</script>
<template>
<li class="timeline-entry">
<div class="timeline-entry-inner"><slot></slot></div>
</li>
</template>
......@@ -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,11 +42,12 @@
padding: 10px;
text-align: right;
float: left;
line-height: 1;
a {
font-family: $monospace-font;
display: block;
font-size: $code_font_size !important;
font-size: $code-font-size !important;
min-height: 19px;
white-space: nowrap;
......
......@@ -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,
......
......@@ -19,3 +19,4 @@ $info: $blue-500;
$warning: $orange-500;
$danger: $red-500;
$zindex-modal-backdrop: 1040;
$nav-divider-margin-y: ($grid-size / 2);
......@@ -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 {
......
......@@ -371,10 +371,10 @@ $note-form-margin-left: 72px;
&::after {
content: '';
width: 100%;
height: 70px;
position: absolute;
left: 0;
left: $gl-padding-24;
right: 0;
bottom: 0;
background: linear-gradient(rgba($white-light, 0.1) -100px, $white-light 100%);
}
......@@ -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;
......
......@@ -144,11 +144,13 @@
.provider-btn-group {
display: inline-block;
margin-right: 10px;
margin-bottom: 10px;
border: 1px solid $border-color;
border-radius: 3px;
&:last-child {
margin-right: 0;
margin-bottom: 0;
}
}
......
......@@ -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;
......
......@@ -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
......
......@@ -100,18 +100,12 @@ module Boards
.merge(board_id: params[:board_id], list_id: params[:list_id], request: request)
end
def serializer
IssueSerializer.new(current_user: current_user)
end
def serialize_as_json(resource)
resource.as_json(
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position, :weight],
labels: true,
issue_endpoints: true,
include_full_project_path: board.group_board?,
include: {
project: { only: [:id, :path] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
milestone: { only: [:id, :title] }
}
)
serializer.represent(resource, serializer: 'board', include_full_project_path: board.group_board?)
end
def whitelist_query_limiting
......
......@@ -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
......@@ -91,7 +91,7 @@ module IssuableCollections
options = {
scope: params[:scope],
state: params[:state],
sort: set_sort_order_from_cookie || default_sort_order
sort: set_sort_order
}
# Used by view to highlight active option
......@@ -113,6 +113,32 @@ module IssuableCollections
'opened'
end
def set_sort_order
set_sort_order_from_user_preference || set_sort_order_from_cookie || default_sort_order
end
def set_sort_order_from_user_preference
return unless current_user
return unless issuable_sorting_field
user_preference = current_user.user_preference
sort_param = params[:sort]
sort_param ||= user_preference[issuable_sorting_field]
if user_preference[issuable_sorting_field] != sort_param
user_preference.update_attribute(issuable_sorting_field, sort_param)
end
sort_param
end
# Implement default_sorting_field method on controllers
# to choose which column to store the sorting parameter.
def issuable_sorting_field
nil
end
def set_sort_order_from_cookie
sort_param = params[:sort] if params[:sort].present?
# fallback to legacy cookie value for backward compatibility
......
# frozen_string_literal: true
module UploadsActions
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
include SendFileUpload
UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze
included do
prepend_before_action :set_html_format, only: :show
end
def create
link_to_file = UploadService.new(model, params[:file], uploader_class).execute
......@@ -61,13 +55,6 @@ module UploadsActions
private
# Explicitly set the format.
# Otherwise rails 5 will set it from a file extension.
# See https://github.com/rails/rails/commit/84e8accd6fb83031e4c27e44925d7596655285f7#diff-2b8f2fbb113b55ca8e16001c393da8f1
def set_html_format
request.format = :html
end
def uploader_class
raise NotImplementedError
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
......
......@@ -4,7 +4,7 @@ class Profiles::AccountsController < Profiles::ApplicationController
include AuthHelper
def show
@user = current_user
render(locals: show_view_variables)
end
# rubocop: disable CodeReuse/ActiveRecord
......@@ -23,4 +23,10 @@ class Profiles::AccountsController < Profiles::ApplicationController
redirect_to profile_account_path
end
# rubocop: enable CodeReuse/ActiveRecord
private
def show_view_variables
{}
end
end
......@@ -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
......
......@@ -9,7 +9,6 @@ class Projects::ArtifactsController < Projects::ApplicationController
before_action :authorize_read_build!
before_action :authorize_update_build!, only: [:keep]
before_action :extract_ref_name_and_path
before_action :set_request_format, only: [:file]
before_action :validate_artifacts!, except: [:download]
before_action :entry, only: [:file]
......@@ -110,12 +109,4 @@ class Projects::ArtifactsController < Projects::ApplicationController
render_404 unless @entry.exists?
end
def set_request_format
request.format = :html if set_request_format?
end
def set_request_format?
request.format != :json
end
end
......@@ -9,7 +9,6 @@ class Projects::BlobController < Projects::ApplicationController
include ActionView::Helpers::SanitizeHelper
prepend_before_action :authenticate_user!, only: [:edit]
before_action :set_request_format, only: [:edit, :show, :update, :destroy]
before_action :require_non_empty_project, except: [:new, :create]
before_action :authorize_download_code!
......@@ -242,18 +241,6 @@ class Projects::BlobController < Projects::ApplicationController
.last_for_path(@repository, @ref, @path).sha
end
# In Rails 4.2 if params[:format] is empty, Rails set it to :html
# But since Rails 5.0 the framework now looks for an extension.
# E.g. for `blob/master/CHANGELOG.md` in Rails 4 the format would be `:html`, but in Rails 5 on it'd be `:md`
# This before_action explicitly sets the `:html` format for all requests unless `:format` is set by a client e.g. by JS for XHR requests.
def set_request_format
request.format = :html if set_request_format?
end
def set_request_format?
params[:id].present? && params[:format].blank? && request.format != "json"
end
def show_html
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
......
......@@ -12,7 +12,6 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :assign_ref_vars, except: :commits_root
before_action :authorize_download_code!
before_action :set_commits, except: :commits_root
before_action :set_request_format, only: :show
def commits_root
redirect_to project_commits_path(@project, @project.default_branch)
......@@ -71,19 +70,6 @@ class Projects::CommitsController < Projects::ApplicationController
@commits = set_commits_for_rendering(@commits)
end
# Rails 5 sets request.format from the extension.
# Explicitly set to :html.
def set_request_format
request.format = :html if set_request_format?
end
# Rails 5 sets request.format from extension.
# In this case if the ref ends with `.atom`, it's expected to be the html response,
# not the atom one. So explicitly set request.format as :html to act like rails4.
def set_request_format?
request.format.to_s == "text/html" || @commits.ref.ends_with?("atom")
end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42330')
end
......
......@@ -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
......
......@@ -57,6 +57,10 @@ module AuthHelper
auth_providers.reject { |provider| form_based_provider?(provider) }
end
def display_providers_on_profile?
button_based_providers.any?
end
def providers_for_base_controller
auth_providers.reject { |provider| LDAP_PROVIDER === provider }
end
......
......@@ -7,7 +7,7 @@ class ApplicationSetting < ActiveRecord::Base
include IgnorableColumn
include ChronicDurationAttribute
add_authentication_token_field :runners_registration_token
add_authentication_token_field :runners_registration_token, encrypted: true, fallback: true
add_authentication_token_field :health_check_access_token
DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
......
......@@ -16,6 +16,7 @@ module Ci
belongs_to :user
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule'
belongs_to :merge_request, class_name: 'MergeRequest'
has_internal_id :iid, scope: :project, presence: false, init: ->(s) do
s&.project&.pipelines&.maximum(:iid) || s&.project&.pipelines&.count
......@@ -26,6 +27,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.
......@@ -48,6 +51,9 @@ module Ci
validates :sha, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? }
validates :merge_request, presence: { if: :merge_request? }
validates :merge_request, absence: { unless: :merge_request? }
validates :tag, inclusion: { in: [false], if: :merge_request? }
validates :status, presence: { unless: :importing? }
validate :valid_commit_sha, unless: :importing?
......@@ -169,6 +175,15 @@ module Ci
scope :internal, -> { where(source: internal_sources) }
scope :sort_by_merge_request_pipelines, -> do
sql = 'CASE ci_pipelines.source WHEN (?) THEN 0 ELSE 1 END, ci_pipelines.id DESC'
query = ActiveRecord::Base.send(:sanitize_sql_array, [sql, sources[:merge_request]]) # rubocop:disable GitlabSecurity/PublicSend
order(query)
end
scope :for_user, -> (user) { where(user: user) }
# Returns the pipelines in descending order (= newest first), optionally
# limited to a number of references.
#
......@@ -368,7 +383,7 @@ module Ci
end
def branch?
!tag?
!tag? && !merge_request?
end
def stuck?
......@@ -494,6 +509,8 @@ module Ci
end
def ci_yaml_file_path
return unless repository_source? || unknown_source?
if project.ci_config_path.blank?
'.gitlab-ci.yml'
else
......@@ -523,10 +540,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
......@@ -617,7 +630,12 @@ module Ci
# All the merge requests for which the current pipeline runs/ran against
def all_merge_requests
@all_merge_requests ||= project.merge_requests.where(source_branch: ref)
@all_merge_requests ||=
if merge_request?
project.merge_requests.where(id: merge_request.id)
else
project.merge_requests.where(source_branch: ref)
end
end
def detailed_status(current_user)
......@@ -666,6 +684,7 @@ module Ci
def ci_yaml_from_repo
return unless project
return unless sha
return unless ci_yaml_file_path
project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
rescue GRPC::NotFound, GRPC::Internal
......@@ -693,6 +712,8 @@ module Ci
def git_ref
if branch?
Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
elsif merge_request?
Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
elsif tag?
Gitlab::Git::TAG_REF_PREFIX + ref.to_s
else
......
......@@ -21,7 +21,8 @@ module Ci
trigger: 3,
schedule: 4,
api: 5,
external: 6
external: 6,
merge_request: 10
}
end
end
......
......@@ -8,6 +8,9 @@ module Ci
include RedisCacheable
include ChronicDurationAttribute
include FromUnion
include TokenAuthenticatable
add_authentication_token_field :token, encrypted: true, migrating: true
enum access_level: {
not_protected: 0,
......@@ -39,7 +42,7 @@ module Ci
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
before_validation :set_default_values
before_save :ensure_token
scope :active, -> { where(active: true) }
scope :paused, -> { where(active: false) }
......@@ -145,10 +148,6 @@ module Ci
end
end
def set_default_values
self.token = SecureRandom.hex(15) if self.token.blank?
end
def assign_to(project, current_user = nil)
if instance_type?
self.runner_type = :project_type
......
......@@ -56,7 +56,11 @@ module Clusters
def specification
{
"ingress" => {
"hosts" => [hostname]
"hosts" => [hostname],
"tls" => [{
"hosts" => [hostname],
"secretName" => "jupyter-cert"
}]
},
"hub" => {
"extraEnv" => {
......
......@@ -33,14 +33,12 @@ module Clusters
end
def predefined_variables
config = YAML.dump(kubeconfig)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables
.append(key: 'KUBE_SERVICE_ACCOUNT', value: service_account_name.to_s)
.append(key: 'KUBE_NAMESPACE', value: namespace.to_s)
.append(key: 'KUBE_TOKEN', value: service_account_token.to_s, public: false)
.append(key: 'KUBECONFIG', value: config, public: false, file: true)
.append(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true)
end
end
......
......@@ -85,18 +85,16 @@ module Clusters
if kubernetes_namespace = cluster.kubernetes_namespaces.has_service_account_token.find_by(project: project)
variables.concat(kubernetes_namespace.predefined_variables)
else
elsif cluster.project_type?
# From 11.5, every Clusters::Project should have at least one
# Clusters::KubernetesNamespace, so once migration has been completed,
# this 'else' branch will be removed. For more information, please see
# https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22433
config = YAML.dump(kubeconfig)
variables
.append(key: 'KUBE_URL', value: api_url)
.append(key: 'KUBE_TOKEN', value: token, public: false)
.append(key: 'KUBE_NAMESPACE', value: actual_namespace)
.append(key: 'KUBECONFIG', value: config, public: false, file: true)
.append(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true)
end
end
end
......
......@@ -9,24 +9,18 @@ module TokenAuthenticatable
private # rubocop:disable Lint/UselessAccessModifier
def add_authentication_token_field(token_field, options = {})
@token_fields = [] unless @token_fields
unique = options.fetch(:unique, true)
if @token_fields.include?(token_field)
if token_authenticatable_fields.include?(token_field)
raise ArgumentError.new("#{token_field} already configured via add_authentication_token_field")
end
@token_fields << token_field
token_authenticatable_fields.push(token_field)
attr_accessor :cleartext_tokens
strategy = if options[:digest]
TokenAuthenticatableStrategies::Digest.new(self, token_field, options)
else
TokenAuthenticatableStrategies::Insecure.new(self, token_field, options)
end
strategy = TokenAuthenticatableStrategies::Base
.fabricate(self, token_field, options)
if unique
if options.fetch(:unique, true)
define_singleton_method("find_by_#{token_field}") do |token|
strategy.find_token_authenticatable(token)
end
......@@ -53,6 +47,15 @@ module TokenAuthenticatable
define_method("reset_#{token_field}!") do
strategy.reset_token!(self)
end
define_method("#{token_field}_matches?") do |other_token|
token = read_attribute(token_field)
token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(other_token, token)
end
end
def token_authenticatable_fields
@token_authenticatable_fields ||= []
end
end
end
......@@ -2,6 +2,8 @@
module TokenAuthenticatableStrategies
class Base
attr_reader :klass, :token_field, :options
def initialize(klass, token_field, options)
@klass = klass
@token_field = token_field
......@@ -22,6 +24,7 @@ module TokenAuthenticatableStrategies
def ensure_token(instance)
write_new_token(instance) unless token_set?(instance)
get_token(instance)
end
# Returns a token, but only saves when the database is in read & write mode
......@@ -36,6 +39,36 @@ module TokenAuthenticatableStrategies
instance.save! if Gitlab::Database.read_write?
end
def fallback?
unless options[:fallback].in?([true, false, nil])
raise ArgumentError, 'fallback: needs to be a boolean value!'
end
options[:fallback] == true
end
def migrating?
unless options[:migrating].in?([true, false, nil])
raise ArgumentError, 'migrating: needs to be a boolean value!'
end
options[:migrating] == true
end
def self.fabricate(model, field, options)
if options[:digest] && options[:encrypted]
raise ArgumentError, 'Incompatible options set!'
end
if options[:digest]
TokenAuthenticatableStrategies::Digest.new(model, field, options)
elsif options[:encrypted]
TokenAuthenticatableStrategies::Encrypted.new(model, field, options)
else
TokenAuthenticatableStrategies::Insecure.new(model, field, options)
end
end
protected
def write_new_token(instance)
......@@ -65,9 +98,5 @@ module TokenAuthenticatableStrategies
def token_set?(instance)
raise NotImplementedError
end
def token_field_name
@token_field
end
end
end
# frozen_string_literal: true
module TokenAuthenticatableStrategies
class Encrypted < Base
def initialize(*)
super
if migrating? && fallback?
raise ArgumentError, '`fallback` and `migrating` options are not compatible!'
end
end
def find_token_authenticatable(token, unscoped = false)
return if token.blank?
if fully_encrypted?
return find_by_encrypted_token(token, unscoped)
end
if fallback?
find_by_encrypted_token(token, unscoped) ||
find_by_plaintext_token(token, unscoped)
elsif migrating?
find_by_plaintext_token(token, unscoped)
else
raise ArgumentError, 'Unknown encryption phase!'
end
end
def ensure_token(instance)
# TODO, tech debt, because some specs are testing migrations, but are still
# using factory bot to create resources, it might happen that a database
# schema does not have "#{token_name}_encrypted" field yet, however a bunch
# of models call `ensure_#{token_name}` in `before_save`.
#
# In that case we are using insecure strategy, but this should only happen
# in tests, because otherwise `encrypted_field` is going to exist.
#
# Another use case is when we are caching resources / columns, like we do
# in case of ApplicationSetting.
return super if instance.has_attribute?(encrypted_field)
if fully_encrypted?
raise ArgumentError, 'Using encrypted strategy when encrypted field is missing!'
else
insecure_strategy.ensure_token(instance)
end
end
def get_token(instance)
return insecure_strategy.get_token(instance) if migrating?
encrypted_token = instance.read_attribute(encrypted_field)
token = Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token)
token || (insecure_strategy.get_token(instance) if fallback?)
end
def set_token(instance, token)
raise ArgumentError unless token.present?
instance[encrypted_field] = Gitlab::CryptoHelper.aes256_gcm_encrypt(token)
instance[token_field] = token if migrating?
instance[token_field] = nil if fallback?
token
end
def fully_encrypted?
!migrating? && !fallback?
end
protected
def find_by_plaintext_token(token, unscoped)
insecure_strategy.find_token_authenticatable(token, unscoped)
end
def find_by_encrypted_token(token, unscoped)
encrypted_value = Gitlab::CryptoHelper.aes256_gcm_encrypt(token)
relation(unscoped).find_by(encrypted_field => encrypted_value)
end
def insecure_strategy
@insecure_strategy ||= TokenAuthenticatableStrategies::Insecure
.new(klass, token_field, options)
end
def token_set?(instance)
raw_token = instance.read_attribute(encrypted_field)
unless fully_encrypted?
raw_token ||= insecure_strategy.get_token(instance)
end
raw_token.present?
end
def encrypted_field
@encrypted_field ||= "#{@token_field}_encrypted"
end
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
......
......@@ -55,7 +55,7 @@ class Group < Namespace
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
add_authentication_token_field :runners_token
add_authentication_token_field :runners_token, encrypted: true, migrating: true
after_create :post_create_hook
after_destroy :post_destroy_hook
......
......@@ -6,12 +6,12 @@ class WebHook < ActiveRecord::Base
attr_encrypted :token,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
key: Settings.attr_encrypted_db_key_base_truncated
key: Settings.attr_encrypted_db_key_base_32
attr_encrypted :url,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
key: Settings.attr_encrypted_db_key_base_truncated
key: Settings.attr_encrypted_db_key_base_32
has_many :web_hook_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
......
......@@ -235,20 +235,6 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
if options.key?(:issue_endpoints) && project
url_helper = Gitlab::Routing.url_helpers
issue_reference = options[:include_full_project_path] ? to_reference(full: true) : to_reference
json.merge!(
reference_path: issue_reference,
real_path: url_helper.project_issue_path(project, self),
issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'),
toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self),
assignable_labels_endpoint: url_helper.project_labels_path(project, format: :json, include_ancestor_groups: true)
)
end
if options.key?(:labels)
json[:labels] = labels.as_json(
project: project,
......
......@@ -63,6 +63,7 @@ class MergeRequest < ActiveRecord::Base
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :cached_closes_issues, through: :merge_requests_closing_issues, source: :issue
has_many :merge_request_pipelines, foreign_key: 'merge_request_id', class_name: 'Ci::Pipeline'
belongs_to :assignee, class_name: "User"
......@@ -1052,12 +1053,17 @@ class MergeRequest < ActiveRecord::Base
diverged_commits_count > 0
end
def all_pipelines
def all_pipelines(shas: all_commit_shas)
return Ci::Pipeline.none unless source_project
@all_pipelines ||= source_project.pipelines
.where(sha: all_commit_shas, ref: source_branch)
.order(id: :desc)
.where(sha: shas, ref: source_branch)
.where(merge_request: [nil, self])
.sort_by_merge_request_pipelines
end
def merge_request_pipeline_exists?
merge_request_pipelines.exists?(sha: diff_head_sha)
end
def has_test_reports?
......
......@@ -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]
......
......@@ -85,7 +85,7 @@ class Project < ActiveRecord::Base
default_value_for :snippets_enabled, gitlab_config_features.snippets
default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
add_authentication_token_field :runners_token
add_authentication_token_field :runners_token, encrypted: true, migrating: true
before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
......@@ -1140,6 +1140,11 @@ class Project < ActiveRecord::Base
"#{web_url}.git"
end
# Is overriden in EE
def lfs_http_url_to_repo(_)
http_url_to_repo
end
def forked?
fork_network && fork_network.root_project != self
end
......
......@@ -110,14 +110,12 @@ class KubernetesService < DeploymentService
# Clusters::Platforms::Kubernetes, it won't be used on this method
# as it's only needed for Clusters::Cluster.
def predefined_variables(project:)
config = YAML.dump(kubeconfig)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables
.append(key: 'KUBE_URL', value: api_url)
.append(key: 'KUBE_TOKEN', value: token, public: false)
.append(key: 'KUBE_NAMESPACE', value: actual_namespace)
.append(key: 'KUBECONFIG', value: config, public: false, file: true)
.append(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true)
if ca_pem.present?
variables
......
......@@ -180,7 +180,7 @@ def index
render json: MyResourceSerializer
.new(current_user: @current_user)
.represent_details(@project.resources)
nd
end
end
```
......@@ -196,7 +196,7 @@ def index
.represent_details(@project.resources),
count: @project.resources.count
}
nd
end
end
```
......
# frozen_string_literal: true
class IssueBoardEntity < Grape::Entity
include RequestAwareEntity
expose :id
expose :iid
expose :title
expose :confidential
expose :due_date
expose :project_id
expose :relative_position
expose :project do |issue|
API::Entities::Project.represent issue.project, only: [:id, :path]
end
expose :milestone, expose_nil: false do |issue|
API::Entities::Project.represent issue.milestone, only: [:id, :title]
end
expose :assignees do |issue|
API::Entities::UserBasic.represent issue.assignees, only: [:id, :name, :username, :avatar_url]
end
expose :labels do |issue|
LabelEntity.represent issue.labels, project: issue.project, only: [:id, :title, :description, :color, :priority, :text_color]
end
expose :reference_path, if: -> (issue) { issue.project } do |issue, options|
options[:include_full_project_path] ? issue.to_reference(full: true) : issue.to_reference
end
expose :real_path, if: -> (issue) { issue.project } do |issue|
project_issue_path(issue.project, issue)
end
expose :issue_sidebar_endpoint, if: -> (issue) { issue.project } do |issue|
project_issue_path(issue.project, issue, format: :json, serializer: 'sidebar')
end
expose :toggle_subscription_endpoint, if: -> (issue) { issue.project } do |issue|
toggle_subscription_project_issue_path(issue.project, issue)
end
expose :assignable_labels_endpoint, if: -> (issue) { issue.project } do |issue|
project_labels_path(issue.project, format: :json, include_ancestor_groups: true)
end
end
......@@ -4,15 +4,17 @@ class IssueSerializer < BaseSerializer
# This overrided method takes care of which entity should be used
# to serialize the `issue` based on `basic` key in `opts` param.
# Hence, `entity` doesn't need to be declared on the class scope.
def represent(merge_request, opts = {})
def represent(issue, opts = {})
entity =
case opts[:serializer]
when 'sidebar'
IssueSidebarEntity
when 'board'
IssueBoardEntity
else
IssueEntity
end
super(merge_request, opts, entity)
super(issue, opts, entity)
end
end
......@@ -12,4 +12,8 @@ class LabelEntity < Grape::Entity
expose :text_color
expose :created_at
expose :updated_at
expose :priority, if: -> (*) { options.key?(:project) } do |label|
label.priority(options[:project])
end
end
......@@ -48,6 +48,7 @@ class PipelineEntity < Grape::Entity
expose :tag?, as: :tag
expose :branch?, as: :branch
expose :merge_request?, as: :merge_request
end
expose :commit, using: CommitEntity
......
......@@ -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,
......@@ -12,7 +14,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Populate,
Gitlab::Ci::Pipeline::Chain::Create].freeze
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block)
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, &block)
@pipeline = Ci::Pipeline.new
command = Gitlab::Ci::Pipeline::Chain::Command.new(
......@@ -23,6 +25,7 @@ module Ci
before_sha: params[:before],
trigger_request: trigger_request,
schedule: schedule,
merge_request: merge_request,
ignore_skip_ci: ignore_skip_ci,
save_incompleted: save_on_errors,
seeds_block: block,
......@@ -47,6 +50,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
......
......@@ -12,7 +12,8 @@ module Clusters
create_gitlab_service_account!
configure_kubernetes
cluster.save!
configure_project_service_account
ClusterPlatformConfigureWorker.perform_async(cluster.id)
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
......@@ -25,7 +26,7 @@ module Clusters
private
def create_gitlab_service_account!
Clusters::Gcp::Kubernetes::CreateServiceAccountService.gitlab_creator(
Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService.gitlab_creator(
kube_client,
rbac: create_rbac_cluster?
).execute
......@@ -55,15 +56,6 @@ module Clusters
).execute
end
def configure_project_service_account
kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace(cluster.cluster_project)
Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new(
cluster: cluster,
kubernetes_namespace: kubernetes_namespace
).execute
end
def authorization_type
create_rbac_cluster? ? 'rbac' : 'abac'
end
......
......@@ -27,7 +27,7 @@ module Clusters
end
def create_project_service_account
Clusters::Gcp::Kubernetes::CreateServiceAccountService.namespace_creator(
Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService.namespace_creator(
platform.kubeclient,
service_account_name: kubernetes_namespace.service_account_name,
service_account_namespace: kubernetes_namespace.namespace,
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment