Commit 6b4926f2 authored by Grzegorz Bizon's avatar Grzegorz Bizon

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

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