Commit af2c51d7 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'master-ce' into scheduled-manual-jobs

parents acfe7ec6 f71c497f
...@@ -606,7 +606,7 @@ static-analysis: ...@@ -606,7 +606,7 @@ static-analysis:
docs lint: docs lint:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-qa <<: *except-qa
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine" image: "registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-docs-lint"
stage: test stage: test
cache: {} cache: {}
dependencies: [] dependencies: []
...@@ -614,8 +614,8 @@ docs lint: ...@@ -614,8 +614,8 @@ docs lint:
script: script:
- scripts/lint-doc.sh - scripts/lint-doc.sh
- scripts/lint-changelog-yaml - scripts/lint-changelog-yaml
- mv doc/ /nanoc/content/ - mv doc/ /tmp/gitlab-docs/content/
- cd /nanoc - cd /tmp/gitlab-docs
# Build HTML from Markdown # Build HTML from Markdown
- bundle exec nanoc - bundle exec nanoc
# Check the internal links # Check the internal links
......
...@@ -10,24 +10,6 @@ ...@@ -10,24 +10,6 @@
Capybara/CurrentPathExpectation: Capybara/CurrentPathExpectation:
Enabled: false Enabled: false
# Offense count: 23
FactoryBot/DynamicAttributeDefinedStatically:
Exclude:
- 'spec/factories/broadcast_messages.rb'
- 'spec/factories/ci/builds.rb'
- 'spec/factories/ci/runners.rb'
- 'spec/factories/clusters/applications/helm.rb'
- 'spec/factories/clusters/platforms/kubernetes.rb'
- 'spec/factories/emails.rb'
- 'spec/factories/gpg_keys.rb'
- 'spec/factories/group_members.rb'
- 'spec/factories/merge_requests.rb'
- 'spec/factories/notes.rb'
- 'spec/factories/oauth_access_grants.rb'
- 'spec/factories/project_members.rb'
- 'spec/factories/todos.rb'
- 'spec/factories/uploads.rb'
# Offense count: 167 # Offense count: 167
# Cop supports --auto-correct. # Cop supports --auto-correct.
Layout/EmptyLinesAroundArguments: Layout/EmptyLinesAroundArguments:
...@@ -53,20 +35,6 @@ Layout/IndentArray: ...@@ -53,20 +35,6 @@ Layout/IndentArray:
Layout/IndentHash: Layout/IndentHash:
Enabled: false Enabled: false
# Offense count: 11
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment.
Layout/SpaceBeforeFirstArg:
Exclude:
- 'config/routes/project.rb'
- 'db/migrate/20170506185517_add_foreign_key_pipeline_schedules_and_pipelines.rb'
- 'features/steps/project/source/browse_files.rb'
- 'features/steps/project/source/markdown_render.rb'
- 'lib/api/runners.rb'
- 'spec/features/search/user_uses_search_filters_spec.rb'
- 'spec/routing/project_routing_spec.rb'
- 'spec/services/system_note_service_spec.rb'
# Offense count: 93 # Offense count: 93
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
...@@ -74,15 +42,6 @@ Layout/SpaceBeforeFirstArg: ...@@ -74,15 +42,6 @@ Layout/SpaceBeforeFirstArg:
Layout/SpaceInLambdaLiteral: Layout/SpaceInLambdaLiteral:
Enabled: false Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets.
# SupportedStyles: space, no_space, compact
# SupportedStylesForEmptyBrackets: space, no_space
Layout/SpaceInsideArrayLiteralBrackets:
Exclude:
- 'spec/lib/gitlab/import_export/relation_factory_spec.rb'
# Offense count: 327 # Offense count: 327
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
...@@ -96,14 +55,6 @@ Layout/SpaceInsideBlockBraces: ...@@ -96,14 +55,6 @@ Layout/SpaceInsideBlockBraces:
Layout/SpaceInsideParens: Layout/SpaceInsideParens:
Enabled: false Enabled: false
# Offense count: 14
# Cop supports --auto-correct.
Layout/SpaceInsidePercentLiteralDelimiters:
Exclude:
- 'lib/gitlab/git_access.rb'
- 'lib/gitlab/health_checks/fs_shards_check.rb'
- 'spec/lib/gitlab/health_checks/fs_shards_check_spec.rb'
# Offense count: 26 # Offense count: 26
Lint/DuplicateMethods: Lint/DuplicateMethods:
Exclude: Exclude:
...@@ -135,31 +86,11 @@ Lint/InterpolationCheck: ...@@ -135,31 +86,11 @@ Lint/InterpolationCheck:
Lint/MissingCopEnableDirective: Lint/MissingCopEnableDirective:
Enabled: false Enabled: false
# Offense count: 2
Lint/NestedPercentLiteral:
Exclude:
- 'lib/gitlab/git/repository.rb'
- 'spec/support/shared_examples/email_format_shared_examples.rb'
# Offense count: 1 # Offense count: 1
Lint/ReturnInVoidContext: Lint/ReturnInVoidContext:
Exclude: Exclude:
- 'app/models/project.rb' - 'app/models/project.rb'
# Offense count: 1
# Configuration parameters: IgnoreImplicitReferences.
Lint/ShadowedArgument:
Exclude:
- 'lib/gitlab/database/sha_attribute.rb'
# Offense count: 3
# Cop supports --auto-correct.
Lint/UnneededRequireStatement:
Exclude:
- 'db/post_migrate/20161221153951_rename_reserved_project_names.rb'
- 'db/post_migrate/20170313133418_rename_more_reserved_project_names.rb'
- 'lib/declarative_policy.rb'
# Offense count: 9 # Offense count: 9
Lint/UriEscapeUnescape: Lint/UriEscapeUnescape:
Exclude: Exclude:
...@@ -199,16 +130,6 @@ Naming/HeredocDelimiterCase: ...@@ -199,16 +130,6 @@ Naming/HeredocDelimiterCase:
Naming/HeredocDelimiterNaming: Naming/HeredocDelimiterNaming:
Enabled: false Enabled: false
# Offense count: 1
Performance/UnfreezeString:
Exclude:
- 'features/steps/project/commits/commits.rb'
# Offense count: 1
# Cop supports --auto-correct.
Performance/UriDefaultParser:
Exclude:
- 'lib/gitlab/url_sanitizer.rb'
# Offense count: 3821 # Offense count: 3821
# Configuration parameters: Prefixes. # Configuration parameters: Prefixes.
......
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
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.3.3 (2018-10-04)
- No changes.
## 11.3.2 (2018-10-03) ## 11.3.2 (2018-10-03)
### Fixed (4 changes) ### Fixed (4 changes)
......
...@@ -295,6 +295,7 @@ gem 'peek-mysql2', '~> 1.1.0', group: :mysql ...@@ -295,6 +295,7 @@ gem 'peek-mysql2', '~> 1.1.0', group: :mysql
gem 'peek-pg', '~> 1.3.0', group: :postgres gem 'peek-pg', '~> 1.3.0', group: :postgres
gem 'peek-rblineprof', '~> 0.2.0' gem 'peek-rblineprof', '~> 0.2.0'
gem 'peek-redis', '~> 1.2.0' gem 'peek-redis', '~> 1.2.0'
gem 'gitlab-sidekiq-fetcher', require: 'sidekiq-reliable-fetch'
# Metrics # Metrics
group :metrics do group :metrics do
......
...@@ -301,6 +301,8 @@ GEM ...@@ -301,6 +301,8 @@ GEM
mime-types (>= 1.16) mime-types (>= 1.16)
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab-markup (1.6.4) gitlab-markup (1.6.4)
gitlab-sidekiq-fetcher (0.3.0)
sidekiq (~> 5)
gitlab-styles (2.4.1) gitlab-styles (2.4.1)
rubocop (~> 0.54.0) rubocop (~> 0.54.0)
rubocop-gitlab-security (~> 0.1.0) rubocop-gitlab-security (~> 0.1.0)
...@@ -1031,6 +1033,7 @@ DEPENDENCIES ...@@ -1031,6 +1033,7 @@ DEPENDENCIES
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2) gitlab-gollum-lib (~> 4.2)
gitlab-markup (~> 1.6.4) gitlab-markup (~> 1.6.4)
gitlab-sidekiq-fetcher
gitlab-styles (~> 2.4) gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4) gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2) gon (~> 6.2)
......
...@@ -304,6 +304,8 @@ GEM ...@@ -304,6 +304,8 @@ GEM
mime-types (>= 1.16) mime-types (>= 1.16)
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab-markup (1.6.4) gitlab-markup (1.6.4)
gitlab-sidekiq-fetcher (0.3.0)
sidekiq (~> 5)
gitlab-styles (2.4.1) gitlab-styles (2.4.1)
rubocop (~> 0.54.0) rubocop (~> 0.54.0)
rubocop-gitlab-security (~> 0.1.0) rubocop-gitlab-security (~> 0.1.0)
...@@ -1040,6 +1042,7 @@ DEPENDENCIES ...@@ -1040,6 +1042,7 @@ DEPENDENCIES
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2) gitlab-gollum-lib (~> 4.2)
gitlab-markup (~> 1.6.4) gitlab-markup (~> 1.6.4)
gitlab-sidekiq-fetcher
gitlab-styles (~> 2.4) gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4) gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2) gon (~> 6.2)
......
...@@ -22,6 +22,7 @@ const Api = { ...@@ -22,6 +22,7 @@ const Api = {
dockerfilePath: '/api/:version/templates/dockerfiles/:key', dockerfilePath: '/api/:version/templates/dockerfiles/:key',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json', usersPath: '/api/:version/users.json',
userStatusPath: '/api/:version/user/status',
commitPath: '/api/:version/projects/:id/repository/commits', commitPath: '/api/:version/projects/:id/repository/commits',
commitPipelinesPath: '/:project_id/commit/:sha/pipelines', commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
...@@ -266,6 +267,15 @@ const Api = { ...@@ -266,6 +267,15 @@ const Api = {
}); });
}, },
postUserStatus({ emoji, message }) {
const url = Api.buildUrl(this.userStatusPath);
return axios.put(url, {
emoji,
message,
});
},
templates(key, params = {}) { templates(key, params = {}) {
const url = Api.buildUrl(this.templatesPath).replace(':key', key); const url = Api.buildUrl(this.templatesPath).replace(':key', key);
......
...@@ -42,10 +42,11 @@ export class AwardsHandler { ...@@ -42,10 +42,11 @@ export class AwardsHandler {
} }
bindEvents() { bindEvents() {
const $parentEl = this.targetContainerEl ? $(this.targetContainerEl) : $(document);
// If the user shows intent let's pre-build the menu // If the user shows intent let's pre-build the menu
this.registerEventListener( this.registerEventListener(
'one', 'one',
$(document), $parentEl,
'mouseenter focus', 'mouseenter focus',
this.toggleButtonSelector, this.toggleButtonSelector,
'mouseenter focus', 'mouseenter focus',
...@@ -58,7 +59,7 @@ export class AwardsHandler { ...@@ -58,7 +59,7 @@ export class AwardsHandler {
} }
}, },
); );
this.registerEventListener('on', $(document), 'click', this.toggleButtonSelector, e => { this.registerEventListener('on', $parentEl, 'click', this.toggleButtonSelector, e => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.showEmojiMenu($(e.currentTarget)); this.showEmojiMenu($(e.currentTarget));
...@@ -76,7 +77,7 @@ export class AwardsHandler { ...@@ -76,7 +77,7 @@ export class AwardsHandler {
}); });
const emojiButtonSelector = `.js-awards-block .js-emoji-btn, .${this.menuClass} .js-emoji-btn`; const emojiButtonSelector = `.js-awards-block .js-emoji-btn, .${this.menuClass} .js-emoji-btn`;
this.registerEventListener('on', $(document), 'click', emojiButtonSelector, e => { this.registerEventListener('on', $parentEl, 'click', emojiButtonSelector, e => {
e.preventDefault(); e.preventDefault();
const $target = $(e.currentTarget); const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji'); const $glEmojiElement = $target.find('gl-emoji');
...@@ -168,7 +169,8 @@ export class AwardsHandler { ...@@ -168,7 +169,8 @@ export class AwardsHandler {
</div> </div>
`; `;
document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup); const targetEl = this.targetContainerEl ? this.targetContainerEl : document.body;
targetEl.insertAdjacentHTML('beforeend', emojiMenuMarkup);
this.addRemainingEmojiMenuCategories(); this.addRemainingEmojiMenuCategories();
this.setupSearch(); this.setupSearch();
...@@ -250,6 +252,12 @@ export class AwardsHandler { ...@@ -250,6 +252,12 @@ export class AwardsHandler {
} }
positionMenu($menu, $addBtn) { positionMenu($menu, $addBtn) {
if (this.targetContainerEl) {
return $menu.css({
top: `${$addBtn.outerHeight()}px`,
});
}
const position = $addBtn.data('position'); const position = $addBtn.data('position');
// The menu could potentially be off-screen or in a hidden overflow element // The menu could potentially be off-screen or in a hidden overflow element
// So we position the element absolute in the body // So we position the element absolute in the body
...@@ -424,9 +432,7 @@ export class AwardsHandler { ...@@ -424,9 +432,7 @@ export class AwardsHandler {
users = origTitle.trim().split(FROM_SENTENCE_REGEX); users = origTitle.trim().split(FROM_SENTENCE_REGEX);
} }
users.unshift('You'); users.unshift('You');
return awardBlock return awardBlock.attr('title', this.toSentence(users)).tooltip('_fixTitle');
.attr('title', this.toSentence(users))
.tooltip('_fixTitle');
} }
createAwardButtonForVotesBlock(votesBlock, emojiName) { createAwardButtonForVotesBlock(votesBlock, emojiName) {
...@@ -609,13 +615,11 @@ export class AwardsHandler { ...@@ -609,13 +615,11 @@ export class AwardsHandler {
let awardsHandlerPromise = null; let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) { export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) { if (!awardsHandlerPromise || reload) {
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then( awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(Emoji => {
Emoji => { const awardsHandler = new AwardsHandler(Emoji);
const awardsHandler = new AwardsHandler(Emoji); awardsHandler.bindEvents();
awardsHandler.bindEvents(); return awardsHandler;
return awardsHandler; });
},
);
} }
return awardsHandlerPromise; return awardsHandlerPromise;
} }
...@@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue'; ...@@ -5,6 +5,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 CIIcon from '~/vue_shared/components/ci_icon.vue'; import CIIcon from '~/vue_shared/components/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
/** /**
* CommitItem * CommitItem
...@@ -29,6 +30,7 @@ export default { ...@@ -29,6 +30,7 @@ export default {
ClipboardButton, ClipboardButton,
CIIcon, CIIcon,
TimeAgoTooltip, TimeAgoTooltip,
CommitPipelineStatus,
}, },
props: { props: {
commit: { commit: {
...@@ -102,6 +104,14 @@ export default { ...@@ -102,6 +104,14 @@ export default {
></pre> ></pre>
</div> </div>
<div class="commit-actions flex-row d-none d-sm-flex"> <div class="commit-actions flex-row d-none d-sm-flex">
<div
v-if="commit.signatureHtml"
v-html="commit.signatureHtml"
></div>
<commit-pipeline-status
v-if="commit.pipelineStatusPath"
:endpoint="commit.pipelineStatusPath"
/>
<div class="commit-sha-group"> <div class="commit-sha-group">
<div <div
class="label label-monospace" class="label label-monospace"
......
...@@ -244,6 +244,7 @@ export function getDiffPositionByLineCode(diffFiles) { ...@@ -244,6 +244,7 @@ export function getDiffPositionByLineCode(diffFiles) {
oldLine, oldLine,
newLine, newLine,
lineCode, lineCode,
positionType: 'text',
}; };
} }
}); });
...@@ -259,8 +260,8 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD ...@@ -259,8 +260,8 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD
const { lineCode, ...diffPositionCopy } = diffPosition; const { lineCode, ...diffPositionCopy } = diffPosition;
if (discussion.original_position && discussion.position) { if (discussion.original_position && discussion.position) {
const originalRefs = convertObjectPropsToCamelCase(discussion.original_position.formatter); const originalRefs = convertObjectPropsToCamelCase(discussion.original_position);
const refs = convertObjectPropsToCamelCase(discussion.position.formatter); const refs = convertObjectPropsToCamelCase(discussion.position);
return _.isEqual(refs, diffPositionCopy) || _.isEqual(originalRefs, diffPositionCopy); return _.isEqual(refs, diffPositionCopy) || _.isEqual(originalRefs, diffPositionCopy);
} }
......
...@@ -466,7 +466,9 @@ export default { ...@@ -466,7 +466,9 @@ export default {
class="gl-responsive-table-row" class="gl-responsive-table-row"
role="row"> role="row">
<div <div
class="table-section section-wrap section-15" v-tooltip
:title="model.name"
class="table-section section-wrap section-15 text-truncate"
role="gridcell" role="gridcell"
> >
<div <div
...@@ -480,9 +482,7 @@ export default { ...@@ -480,9 +482,7 @@ export default {
v-if="!model.isFolder" v-if="!model.isFolder"
class="environment-name table-mobile-content"> class="environment-name table-mobile-content">
<a <a
v-tooltip
:href="environmentPath" :href="environmentPath"
:title="model.name"
> >
{{ model.name }} {{ model.name }}
</a> </a>
......
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { highCountTrim } from '~/lib/utils/text_utility'; import { highCountTrim } from '~/lib/utils/text_utility';
import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue';
import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue';
/** /**
* Updates todo counter when todos are toggled. * Updates todo counter when todos are toggled.
...@@ -17,3 +21,54 @@ export default function initTodoToggle() { ...@@ -17,3 +21,54 @@ export default function initTodoToggle() {
$todoPendingCount.toggleClass('hidden', parsedCount === 0); $todoPendingCount.toggleClass('hidden', parsedCount === 0);
}); });
} }
document.addEventListener('DOMContentLoaded', () => {
const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger');
const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper');
if (setStatusModalTriggerEl || setStatusModalWrapperEl) {
Vue.use(Translate);
// eslint-disable-next-line no-new
new Vue({
el: setStatusModalTriggerEl,
data() {
const { hasStatus } = this.$options.el.dataset;
return {
hasStatus: hasStatus === 'true',
};
},
render(createElement) {
return createElement(SetStatusModalTrigger, {
props: {
hasStatus: this.hasStatus,
},
});
},
});
// eslint-disable-next-line no-new
new Vue({
el: setStatusModalWrapperEl,
data() {
const { currentEmoji, currentMessage } = this.$options.el.dataset;
return {
currentEmoji,
currentMessage,
};
},
render(createElement) {
const { currentEmoji, currentMessage } = this;
return createElement(SetStatusModalWrapper, {
props: {
currentEmoji,
currentMessage,
},
});
},
});
}
});
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
v-if="mergeRequest" v-if="mergeRequest"
:href="mergeRequest.path" :href="mergeRequest.path"
class="js-link-commit link-commit" class="js-link-commit link-commit"
>{{ mergeRequest.iid }}</a> >!{{ mergeRequest.iid }}</a>
</p> </p>
<p class="build-light-text append-bottom-0"> <p class="build-light-text append-bottom-0">
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
value === null || value === null ||
(Object.prototype.hasOwnProperty.call(value, 'path') && (Object.prototype.hasOwnProperty.call(value, 'path') &&
Object.prototype.hasOwnProperty.call(value, 'method') && Object.prototype.hasOwnProperty.call(value, 'method') &&
Object.prototype.hasOwnProperty.call(value, 'title')) Object.prototype.hasOwnProperty.call(value, 'button_title'))
); );
}, },
}, },
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
:data-method="action.method" :data-method="action.method"
class="js-job-empty-state-action btn btn-primary" class="js-job-empty-state-action btn btn-primary"
> >
{{ action.title }} {{ action.button_title }}
</a> </a>
</div> </div>
</div> </div>
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { mapGetters, mapState } from 'vuex'; import { mapGetters, mapState } from 'vuex';
import CiHeader from '~/vue_shared/components/header_ci_component.vue'; import CiHeader from '~/vue_shared/components/header_ci_component.vue';
import Callout from '~/vue_shared/components/callout.vue'; import Callout from '~/vue_shared/components/callout.vue';
import EmptyState from './empty_state.vue';
import EnvironmentsBlock from './environments_block.vue'; import EnvironmentsBlock from './environments_block.vue';
import ErasedBlock from './erased_block.vue'; import ErasedBlock from './erased_block.vue';
import StuckBlock from './stuck_block.vue'; import StuckBlock from './stuck_block.vue';
...@@ -11,6 +12,7 @@ ...@@ -11,6 +12,7 @@
components: { components: {
CiHeader, CiHeader,
Callout, Callout,
EmptyState,
EnvironmentsBlock, EnvironmentsBlock,
ErasedBlock, ErasedBlock,
StuckBlock, StuckBlock,
...@@ -31,6 +33,8 @@ ...@@ -31,6 +33,8 @@
'jobHasStarted', 'jobHasStarted',
'hasEnvironment', 'hasEnvironment',
'isJobStuck', 'isJobStuck',
'hasTrace',
'emptyStateIllustration',
]), ]),
}, },
}; };
...@@ -77,12 +81,14 @@ ...@@ -77,12 +81,14 @@
<environments-block <environments-block
v-if="hasEnvironment" v-if="hasEnvironment"
class="js-job-environment"
:deployment-status="job.deployment_status" :deployment-status="job.deployment_status"
:icon-status="job.status" :icon-status="job.status"
/> />
<erased-block <erased-block
v-if="job.erased" v-if="job.erased"
class="js-job-erased"
:user="job.erased_by" :user="job.erased_by"
:erased-at="job.erased_at" :erased-at="job.erased_at"
/> />
...@@ -91,6 +97,15 @@ ...@@ -91,6 +97,15 @@
<!-- EO job log --> <!-- EO job log -->
<!--empty state --> <!--empty state -->
<empty-state
v-if="!hasTrace"
class="js-job-empty-state"
:illustration-path="emptyStateIllustration.image"
:illustration-size-class="emptyStateIllustration.size"
:title="emptyStateIllustration.title"
:content="emptyStateIllustration.content"
:action="job.status.action"
/>
<!-- EO empty state --> <!-- EO empty state -->
<!-- EO Body Section --> <!-- EO Body Section -->
......
...@@ -29,6 +29,16 @@ export const jobHasStarted = state => !(state.job.started === false); ...@@ -29,6 +29,16 @@ export const jobHasStarted = state => !(state.job.started === false);
export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status); export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);
/**
* Checks if it the job has trace.
* Used to check if it should render the job log or the empty state
* @returns {Boolean}
*/
export const hasTrace = state => state.job.has_trace || state.job.status.group === 'running';
export const emptyStateIllustration = state =>
(state.job && state.job.status && state.job.status.illustration) || {};
/** /**
* When the job is pending and there are no available runners * When the job is pending and there are no available runners
* we need to render the stuck block; * we need to render the stuck block;
...@@ -36,7 +46,8 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status); ...@@ -36,7 +46,8 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);
* @returns {Boolean} * @returns {Boolean}
*/ */
export const isJobStuck = state => export const isJobStuck = state =>
state.job.status.group === 'pending' && state.job.runners && state.job.runners.available === false; state.job.status.group === 'pending' &&
(!_.isEmpty(state.job.runners) && state.job.runners.available === false);
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -126,8 +126,8 @@ export const unresolvedDiscussionsIdsByDiff = (state, getters) => ...@@ -126,8 +126,8 @@ export const unresolvedDiscussionsIdsByDiff = (state, getters) =>
const filenameComparison = a.diff_file.file_path.localeCompare(b.diff_file.file_path); const filenameComparison = a.diff_file.file_path.localeCompare(b.diff_file.file_path);
// Get the line numbers, to compare within the same file // Get the line numbers, to compare within the same file
const aLines = [a.position.formatter.new_line, a.position.formatter.old_line]; const aLines = [a.position.new_line, a.position.old_line];
const bLines = [b.position.formatter.new_line, b.position.formatter.old_line]; const bLines = [b.position.new_line, b.position.old_line];
return filenameComparison < 0 || return filenameComparison < 0 ||
(filenameComparison === 0 && (filenameComparison === 0 &&
......
...@@ -79,10 +79,13 @@ export default class Todos { ...@@ -79,10 +79,13 @@ export default class Todos {
.then(({ data }) => { .then(({ data }) => {
this.updateRowState(target); this.updateRowState(target);
this.updateBadges(data); this.updateBadges(data);
}).catch(() => flash(__('Error updating todo status.'))); }).catch(() => {
this.updateRowState(target, true);
return flash(__('Error updating todo status.'));
});
} }
updateRowState(target) { updateRowState(target, isInactive = false) {
const row = target.closest('li'); const row = target.closest('li');
const restoreBtn = row.querySelector('.js-undo-todo'); const restoreBtn = row.querySelector('.js-undo-todo');
const doneBtn = row.querySelector('.js-done-todo'); const doneBtn = row.querySelector('.js-done-todo');
...@@ -91,7 +94,10 @@ export default class Todos { ...@@ -91,7 +94,10 @@ export default class Todos {
target.removeAttribute('disabled'); target.removeAttribute('disabled');
target.classList.remove('disabled'); target.classList.remove('disabled');
if (target === doneBtn) { if (isInactive === true) {
restoreBtn.classList.add('hidden');
doneBtn.classList.remove('hidden');
} else if (target === doneBtn) {
row.classList.add('done-reversible'); row.classList.add('done-reversible');
restoreBtn.classList.remove('hidden'); restoreBtn.classList.remove('hidden');
} else if (target === restoreBtn) { } else if (target === restoreBtn) {
......
...@@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => {
const statusEmojiField = document.getElementById('js-status-emoji-field'); const statusEmojiField = document.getElementById('js-status-emoji-field');
const statusMessageField = document.getElementById('js-status-message-field'); const statusMessageField = document.getElementById('js-status-message-field');
const toggleNoEmojiPlaceholder = (isVisible) => { const toggleNoEmojiPlaceholder = isVisible => {
const placeholderElement = document.getElementById('js-no-emoji-placeholder'); const placeholderElement = document.getElementById('js-no-emoji-placeholder');
placeholderElement.classList.toggle('hidden', !isVisible); placeholderElement.classList.toggle('hidden', !isVisible);
}; };
...@@ -69,5 +69,5 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -69,5 +69,5 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
}) })
.catch(() => createFlash('Failed to load emoji list!')); .catch(() => createFlash('Failed to load emoji list.'));
}); });
...@@ -43,7 +43,15 @@ const initColorKey = () => ...@@ -43,7 +43,15 @@ const initColorKey = () =>
.domain([0, 3]); .domain([0, 3]);
export default class ActivityCalendar { export default class ActivityCalendar {
constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0, firstDayOfWeek = 0) { constructor(
container,
activitiesContainer,
timestamps,
calendarActivitiesPath,
utcOffset = 0,
firstDayOfWeek = 0,
monthsAgo = 12,
) {
this.calendarActivitiesPath = calendarActivitiesPath; this.calendarActivitiesPath = calendarActivitiesPath;
this.clickDay = this.clickDay.bind(this); this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = ''; this.currentSelectedDate = '';
...@@ -66,6 +74,8 @@ export default class ActivityCalendar { ...@@ -66,6 +74,8 @@ export default class ActivityCalendar {
]; ];
this.months = []; this.months = [];
this.firstDayOfWeek = firstDayOfWeek; this.firstDayOfWeek = firstDayOfWeek;
this.activitiesContainer = activitiesContainer;
this.container = container;
// Loop through the timestamps to create a group of objects // Loop through the timestamps to create a group of objects
// The group of objects will be grouped based on the day of the week they are // The group of objects will be grouped based on the day of the week they are
...@@ -75,13 +85,13 @@ export default class ActivityCalendar { ...@@ -75,13 +85,13 @@ export default class ActivityCalendar {
const today = getSystemDate(utcOffset); const today = getSystemDate(utcOffset);
today.setHours(0, 0, 0, 0, 0); today.setHours(0, 0, 0, 0, 0);
const oneYearAgo = new Date(today); const timeAgo = new Date(today);
oneYearAgo.setFullYear(today.getFullYear() - 1); timeAgo.setMonth(today.getMonth() - monthsAgo);
const days = getDayDifference(oneYearAgo, today); const days = getDayDifference(timeAgo, today);
for (let i = 0; i <= days; i += 1) { for (let i = 0; i <= days; i += 1) {
const date = new Date(oneYearAgo); const date = new Date(timeAgo);
date.setDate(date.getDate() + i); date.setDate(date.getDate() + i);
const day = date.getDay(); const day = date.getDay();
...@@ -280,7 +290,7 @@ export default class ActivityCalendar { ...@@ -280,7 +290,7 @@ export default class ActivityCalendar {
this.currentSelectedDate.getDate(), this.currentSelectedDate.getDate(),
].join('-'); ].join('-');
$('.user-calendar-activities').html(LOADING_HTML); $(this.activitiesContainer).html(LOADING_HTML);
axios axios
.get(this.calendarActivitiesPath, { .get(this.calendarActivitiesPath, {
...@@ -289,11 +299,11 @@ export default class ActivityCalendar { ...@@ -289,11 +299,11 @@ export default class ActivityCalendar {
}, },
responseType: 'text', responseType: 'text',
}) })
.then(({ data }) => $('.user-calendar-activities').html(data)) .then(({ data }) => $(this.activitiesContainer).html(data))
.catch(() => flash(__('An error occurred while retrieving calendar activity'))); .catch(() => flash(__('An error occurred while retrieving calendar activity')));
} else { } else {
this.currentSelectedDate = ''; this.currentSelectedDate = '';
$('.user-calendar-activities').html(''); $(this.activitiesContainer).html('');
} }
} }
} }
import axios from '~/lib/utils/axios_utils';
export default class UserOverviewBlock {
constructor(options = {}) {
this.container = options.container;
this.url = options.url;
this.limit = options.limit || 20;
this.loadData();
}
loadData() {
const loadingEl = document.querySelector(`${this.container} .loading`);
loadingEl.classList.remove('hide');
axios
.get(this.url, {
params: {
limit: this.limit,
},
})
.then(({ data }) => this.render(data))
.catch(() => loadingEl.classList.add('hide'));
}
render(data) {
const { html, count } = data;
const contentList = document.querySelector(`${this.container} .overview-content-list`);
contentList.innerHTML += html;
const loadingEl = document.querySelector(`${this.container} .loading`);
if (count && count > 0) {
document.querySelector(`${this.container} .js-view-all`).classList.remove('hide');
} else {
document.querySelector(`${this.container} .nothing-here-block`).classList.add('text-left', 'p-0');
}
loadingEl.classList.add('hide');
}
}
...@@ -2,9 +2,10 @@ import $ from 'jquery'; ...@@ -2,9 +2,10 @@ import $ from 'jquery';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import Activities from '~/activities'; import Activities from '~/activities';
import { localTimeAgo } from '~/lib/utils/datetime_utility'; import { localTimeAgo } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale'; import { __, sprintf } from '~/locale';
import flash from '~/flash'; import flash from '~/flash';
import ActivityCalendar from './activity_calendar'; import ActivityCalendar from './activity_calendar';
import UserOverviewBlock from './user_overview_block';
/** /**
* UserTabs * UserTabs
...@@ -61,19 +62,28 @@ import ActivityCalendar from './activity_calendar'; ...@@ -61,19 +62,28 @@ import ActivityCalendar from './activity_calendar';
* </div> * </div>
*/ */
const CALENDAR_TEMPLATE = ` const CALENDAR_TEMPLATES = {
<div class="clearfix calendar"> activity: `
<div class="js-contrib-calendar"></div> <div class="clearfix calendar">
<div class="calendar-hint"> <div class="js-contrib-calendar"></div>
Summary of issues, merge requests, push events, and comments <div class="calendar-hint bottom-right"></div>
</div> </div>
</div> `,
`; overview: `
<div class="clearfix calendar">
<div class="calendar-hint"></div>
<div class="js-contrib-calendar prepend-top-20"></div>
</div>
`,
};
const CALENDAR_PERIOD_6_MONTHS = 6;
const CALENDAR_PERIOD_12_MONTHS = 12;
export default class UserTabs { export default class UserTabs {
constructor({ defaultAction, action, parentEl }) { constructor({ defaultAction, action, parentEl }) {
this.loaded = {}; this.loaded = {};
this.defaultAction = defaultAction || 'activity'; this.defaultAction = defaultAction || 'overview';
this.action = action || this.defaultAction; this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document); this.$parentEl = $(parentEl) || $(document);
this.windowLocation = window.location; this.windowLocation = window.location;
...@@ -124,6 +134,8 @@ export default class UserTabs { ...@@ -124,6 +134,8 @@ export default class UserTabs {
} }
if (action === 'activity') { if (action === 'activity') {
this.loadActivities(); this.loadActivities();
} else if (action === 'overview') {
this.loadOverviewTab();
} }
const loadableActions = ['groups', 'contributed', 'projects', 'snippets']; const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
...@@ -154,7 +166,40 @@ export default class UserTabs { ...@@ -154,7 +166,40 @@ export default class UserTabs {
if (this.loaded.activity) { if (this.loaded.activity) {
return; return;
} }
const $calendarWrap = this.$parentEl.find('.user-calendar');
this.loadActivityCalendar('activity');
// eslint-disable-next-line no-new
new Activities();
this.loaded.activity = true;
}
loadOverviewTab() {
if (this.loaded.overview) {
return;
}
this.loadActivityCalendar('overview');
UserTabs.renderMostRecentBlocks('#js-overview .activities-block', 5);
UserTabs.renderMostRecentBlocks('#js-overview .projects-block', 10);
this.loaded.overview = true;
}
static renderMostRecentBlocks(container, limit) {
// eslint-disable-next-line no-new
new UserOverviewBlock({
container,
url: $(`${container} .overview-content-list`).data('href'),
limit,
});
}
loadActivityCalendar(action) {
const monthsAgo = action === 'overview' ? CALENDAR_PERIOD_6_MONTHS : CALENDAR_PERIOD_12_MONTHS;
const $calendarWrap = this.$parentEl.find('.tab-pane.active .user-calendar');
const calendarPath = $calendarWrap.data('calendarPath'); const calendarPath = $calendarWrap.data('calendarPath');
const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath'); const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath');
const utcOffset = $calendarWrap.data('utcOffset'); const utcOffset = $calendarWrap.data('utcOffset');
...@@ -166,17 +211,22 @@ export default class UserTabs { ...@@ -166,17 +211,22 @@ export default class UserTabs {
axios axios
.get(calendarPath) .get(calendarPath)
.then(({ data }) => { .then(({ data }) => {
$calendarWrap.html(CALENDAR_TEMPLATE); $calendarWrap.html(CALENDAR_TEMPLATES[action]);
$calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`);
let calendarHint = '';
if (action === 'activity') {
calendarHint = sprintf(__('Summary of issues, merge requests, push events, and comments (Timezone: %{utcFormatted})'), { utcFormatted });
} else if (action === 'overview') {
calendarHint = __('Issues, merge requests, pushes and comments.');
}
$calendarWrap.find('.calendar-hint').text(calendarHint);
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new ActivityCalendar('.js-contrib-calendar', data, calendarActivitiesPath, utcOffset); new ActivityCalendar('.tab-pane.active .js-contrib-calendar', '.tab-pane.active .user-calendar-activities', data, calendarActivitiesPath, utcOffset, 0, monthsAgo);
}) })
.catch(() => flash(__('There was an error loading users activity calendar.'))); .catch(() => flash(__('There was an error loading users activity calendar.')));
// eslint-disable-next-line no-new
new Activities();
this.loaded.activity = true;
} }
toggleLoading(status) { toggleLoading(status) {
......
import { AwardsHandler } from '~/awards_handler';
class EmojiMenuInModal extends AwardsHandler {
constructor(emoji, toggleButtonSelector, menuClass, selectEmojiCallback, targetContainerEl) {
super(emoji);
this.selectEmojiCallback = selectEmojiCallback;
this.toggleButtonSelector = toggleButtonSelector;
this.menuClass = menuClass;
this.targetContainerEl = targetContainerEl;
this.bindEvents();
}
postEmoji($emojiButton, awardUrl, selectedEmoji, callback) {
this.selectEmojiCallback(selectedEmoji, this.emoji.glEmojiTag(selectedEmoji));
callback();
}
}
export default EmojiMenuInModal;
import Vue from 'vue';
export default new Vue();
<script>
import { s__ } from '~/locale';
import eventHub from './event_hub';
export default {
props: {
hasStatus: {
type: Boolean,
required: true,
},
},
computed: {
buttonText() {
return this.hasStatus ? s__('SetStatusModal|Edit status') : s__('SetStatusModal|Set status');
},
},
methods: {
openModal() {
eventHub.$emit('openModal');
},
},
};
</script>
<template>
<button
type="button"
class="btn menu-item"
@click="openModal"
>
{{ buttonText }}
</button>
</template>
<script>
import $ from 'jquery';
import createFlash from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
import GfmAutoComplete from '~/gfm_auto_complete';
import { __, s__ } from '~/locale';
import Api from '~/api';
import eventHub from './event_hub';
import EmojiMenuInModal from './emoji_menu_in_modal';
const emojiMenuClass = 'js-modal-status-emoji-menu';
export default {
components: {
Icon,
},
props: {
currentEmoji: {
type: String,
required: true,
},
currentMessage: {
type: String,
required: true,
},
},
data() {
return {
defaultEmojiTag: '',
emoji: this.currentEmoji,
emojiMenu: null,
emojiTag: '',
isEmojiMenuVisible: false,
message: this.currentMessage,
modalId: 'set-user-status-modal',
noEmoji: true,
};
},
computed: {
isDirty() {
return this.message.length || this.emoji.length;
},
},
mounted() {
eventHub.$on('openModal', this.openModal);
},
beforeDestroy() {
this.emojiMenu.destroy();
},
methods: {
openModal() {
this.$root.$emit('bv::show::modal', this.modalId);
},
closeModal() {
this.$root.$emit('bv::hide::modal', this.modalId);
},
setupEmojiListAndAutocomplete() {
const toggleEmojiMenuButtonSelector = '#set-user-status-modal .js-toggle-emoji-menu';
const emojiAutocomplete = new GfmAutoComplete();
emojiAutocomplete.setup($(this.$refs.statusMessageField), { emojis: true });
import(/* webpackChunkName: 'emoji' */ '~/emoji')
.then(Emoji => {
if (this.emoji) {
this.emojiTag = Emoji.glEmojiTag(this.emoji);
}
this.noEmoji = this.emoji === '';
this.defaultEmojiTag = Emoji.glEmojiTag('speech_balloon');
this.emojiMenu = new EmojiMenuInModal(
Emoji,
toggleEmojiMenuButtonSelector,
emojiMenuClass,
this.setEmoji,
this.$refs.userStatusForm,
);
})
.catch(() => createFlash(__('Failed to load emoji list.')));
},
showEmojiMenu() {
this.isEmojiMenuVisible = true;
this.emojiMenu.showEmojiMenu($(this.$refs.toggleEmojiMenuButton));
},
hideEmojiMenu() {
if (!this.isEmojiMenuVisible) {
return;
}
this.isEmojiMenuVisible = false;
this.emojiMenu.hideMenuElement($(`.${emojiMenuClass}`));
},
setDefaultEmoji() {
const { emojiTag } = this;
const hasStatusMessage = this.message;
if (hasStatusMessage && emojiTag) {
return;
}
if (hasStatusMessage) {
this.noEmoji = false;
this.emojiTag = this.defaultEmojiTag;
} else if (emojiTag === this.defaultEmojiTag) {
this.noEmoji = true;
this.clearEmoji();
}
},
setEmoji(emoji, emojiTag) {
this.emoji = emoji;
this.noEmoji = false;
this.clearEmoji();
this.emojiTag = emojiTag;
},
clearEmoji() {
if (this.emojiTag) {
this.emojiTag = '';
}
},
clearStatusInputs() {
this.emoji = '';
this.message = '';
this.noEmoji = true;
this.clearEmoji();
this.hideEmojiMenu();
},
removeStatus() {
this.clearStatusInputs();
this.setStatus();
},
setStatus() {
const { emoji, message } = this;
Api.postUserStatus({
emoji,
message,
})
.then(this.onUpdateSuccess)
.catch(this.onUpdateFail);
},
onUpdateSuccess() {
this.closeModal();
window.location.reload();
},
onUpdateFail() {
createFlash(
s__("SetStatusModal|Sorry, we weren't able to set your status. Please try again later."),
);
this.closeModal();
},
},
};
</script>
<template>
<gl-ui-modal
:title="s__('SetStatusModal|Set a status')"
:modal-id="modalId"
:ok-title="s__('SetStatusModal|Set status')"
:cancel-title="s__('SetStatusModal|Remove status')"
ok-variant="success"
class="set-user-status-modal"
@shown="setupEmojiListAndAutocomplete"
@hide="hideEmojiMenu"
@ok="setStatus"
@cancel="removeStatus"
>
<div>
<input
v-model="emoji"
class="js-status-emoji-field"
type="hidden"
name="user[status][emoji]"
/>
<div
ref="userStatusForm"
class="form-group position-relative m-0"
>
<div class="input-group">
<span class="input-group-btn">
<button
ref="toggleEmojiMenuButton"
v-gl-tooltip.bottom
:title="s__('SetStatusModal|Add status emoji')"
:aria-label="s__('SetStatusModal|Add status emoji')"
name="button"
type="button"
class="js-toggle-emoji-menu emoji-menu-toggle-button btn"
@click="showEmojiMenu"
>
<span v-html="emojiTag"></span>
<span
v-show="noEmoji"
class="js-no-emoji-placeholder no-emoji-placeholder position-relative"
>
<icon
name="emoji_slightly_smiling_face"
css-classes="award-control-icon-neutral"
/>
<icon
name="emoji_smiley"
css-classes="award-control-icon-positive"
/>
<icon
name="emoji_smile"
css-classes="award-control-icon-super-positive"
/>
</span>
</button>
</span>
<input
ref="statusMessageField"
v-model="message"
:placeholder="s__('SetStatusModal|What\'s your status?')"
type="text"
class="form-control form-control input-lg js-status-message-field"
name="user[status][message]"
@keyup="setDefaultEmoji"
@keyup.enter.prevent
@click="hideEmojiMenu"
/>
<span
v-show="isDirty"
class="input-group-btn"
>
<button
v-gl-tooltip.bottom
:title="s__('SetStatusModal|Clear status')"
:aria-label="s__('SetStatusModal|Clear status')"
name="button"
type="button"
class="js-clear-user-status-button clear-user-status btn"
@click="clearStatusInputs()"
>
<icon name="close" />
</button>
</span>
</div>
</div>
</div>
</gl-ui-modal>
</template>
...@@ -592,7 +592,7 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -592,7 +592,7 @@ function UsersSelect(currentUser, els, options = {}) {
if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) { if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
var trimmed = query.term.trim(); var trimmed = query.term.trim();
emailUser = { emailUser = {
name: "Invite \"" + query.term + "\" by email", name: "Invite \"" + trimmed + "\" by email",
username: trimmed, username: trimmed,
id: trimmed, id: trimmed,
invite: true invite: true
......
.calender-block { .calendar-block {
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
border-top: 0; border-top: 0;
direction: rtl;
@media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) { @media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) {
overflow-x: auto; overflow-x: auto;
...@@ -42,10 +41,13 @@ ...@@ -42,10 +41,13 @@
} }
.calendar-hint { .calendar-hint {
margin-top: -23px;
float: right;
font-size: 12px; font-size: 12px;
direction: ltr;
&.bottom-right {
direction: ltr;
margin-top: -23px;
float: right;
}
} }
.pika-single.gitlab-theme { .pika-single.gitlab-theme {
......
...@@ -529,9 +529,10 @@ ...@@ -529,9 +529,10 @@
} }
.header-user { .header-user {
.dropdown-menu { &.show .dropdown-menu {
width: auto; width: auto;
min-width: unset; min-width: unset;
max-height: 323px;
margin-top: 4px; margin-top: 4px;
color: $gl-text-color; color: $gl-text-color;
left: auto; left: auto;
...@@ -542,6 +543,18 @@ ...@@ -542,6 +543,18 @@
.user-name { .user-name {
display: block; display: block;
} }
.user-status-emoji {
margin-right: 0;
display: block;
vertical-align: text-top;
max-width: 148px;
font-size: 12px;
gl-emoji {
font-size: $gl-font-size;
}
}
} }
svg { svg {
...@@ -573,3 +586,24 @@ ...@@ -573,3 +586,24 @@
} }
} }
} }
.set-user-status-modal {
.modal-body {
min-height: unset;
}
.input-lg {
max-width: unset;
}
.no-emoji-placeholder,
.clear-user-status {
svg {
fill: $gl-text-color-secondary;
}
}
.emoji-menu-toggle-button {
@include emoji-menu-toggle-button;
}
}
...@@ -20,20 +20,24 @@ ...@@ -20,20 +20,24 @@
display: inline-block; display: inline-block;
overflow-x: auto; overflow-x: auto;
border: 0; border: 0;
border-color: $gray-100; border-color: $gl-gray-100;
@supports (width: fit-content) { @supports (width: fit-content) {
display: block; display: block;
width: fit-content; width: fit-content;
} }
tbody {
background-color: $white-light;
}
tr { tr {
th { th {
border-bottom: solid 2px $gray-100; border-bottom: solid 2px $gl-gray-100;
} }
td { td {
border-color: $gray-100; border-color: $gl-gray-100;
} }
} }
} }
...@@ -266,3 +270,59 @@ ...@@ -266,3 +270,59 @@
border-radius: 50%; border-radius: 50%;
} }
} }
@mixin emoji-menu-toggle-button {
line-height: 1;
padding: 0;
min-width: 16px;
color: $gray-darkest;
fill: $gray-darkest;
.fa {
position: relative;
font-size: 16px;
}
svg {
@include btn-svg;
margin: 0;
}
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
top: 0;
left: 0;
opacity: 0;
}
&:hover,
&.is-active {
.danger-highlight {
color: $red-500;
}
.link-highlight {
color: $blue-600;
fill: $blue-600;
}
.award-control-icon-neutral {
opacity: 0;
}
.award-control-icon-positive {
opacity: 1;
}
}
&.is-active {
.award-control-icon-positive {
opacity: 0;
}
.award-control-icon-super-positive {
opacity: 1;
}
}
}
...@@ -314,7 +314,8 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%); ...@@ -314,7 +314,8 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
$monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', $monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
'Courier New', 'andale mono', 'lucida console', monospace; 'Courier New', 'andale mono', 'lucida console', monospace;
$regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, $regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
/* /*
* Dropdowns * Dropdowns
...@@ -634,5 +635,4 @@ Modals ...@@ -634,5 +635,4 @@ Modals
*/ */
$modal-body-height: 134px; $modal-body-height: 134px;
$priority-label-empty-state-width: 114px; $priority-label-empty-state-width: 114px;
...@@ -18,3 +18,4 @@ $success: $green-500; ...@@ -18,3 +18,4 @@ $success: $green-500;
$info: $blue-500; $info: $blue-500;
$warning: $orange-500; $warning: $orange-500;
$danger: $red-500; $danger: $red-500;
$zindex-modal-backdrop: 1040;
...@@ -519,59 +519,7 @@ ul.notes { ...@@ -519,59 +519,7 @@ ul.notes {
} }
.note-action-button { .note-action-button {
line-height: 1; @include emoji-menu-toggle-button;
padding: 0;
min-width: 16px;
color: $gray-darkest;
fill: $gray-darkest;
.fa {
position: relative;
font-size: 16px;
}
svg {
@include btn-svg;
margin: 0;
}
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
top: 0;
left: 0;
opacity: 0;
}
&:hover,
&.is-active {
.danger-highlight {
color: $red-500;
}
.link-highlight {
color: $blue-600;
fill: $blue-600;
}
.award-control-icon-neutral {
opacity: 0;
}
.award-control-icon-positive {
opacity: 1;
}
}
&.is-active {
.award-control-icon-positive {
opacity: 0;
}
.award-control-icon-super-positive {
opacity: 1;
}
}
} }
.discussion-toggle-button { .discussion-toggle-button {
......
...@@ -81,14 +81,14 @@ ...@@ -81,14 +81,14 @@
// Middle dot divider between each element in a list of items. // Middle dot divider between each element in a list of items.
.middle-dot-divider { .middle-dot-divider {
&::after { &::after {
content: "\00B7"; // Middle Dot content: '\00B7'; // Middle Dot
padding: 0 6px; padding: 0 6px;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
&:last-child { &:last-child {
&::after { &::after {
content: ""; content: '';
padding: 0; padding: 0;
} }
} }
...@@ -191,7 +191,6 @@ ...@@ -191,7 +191,6 @@
@include media-breakpoint-down(xs) { @include media-breakpoint-down(xs) {
width: auto; width: auto;
} }
} }
.profile-crop-image-container { .profile-crop-image-container {
...@@ -215,7 +214,6 @@ ...@@ -215,7 +214,6 @@
} }
} }
.user-profile { .user-profile {
.cover-controls a { .cover-controls a {
margin-left: 5px; margin-left: 5px;
...@@ -418,7 +416,7 @@ table.u2f-registrations { ...@@ -418,7 +416,7 @@ table.u2f-registrations {
} }
&.unverified { &.unverified {
@include status-color($gray-dark, color("gray"), $common-gray-dark); @include status-color($gray-dark, color('gray'), $common-gray-dark);
} }
} }
} }
...@@ -431,7 +429,7 @@ table.u2f-registrations { ...@@ -431,7 +429,7 @@ table.u2f-registrations {
} }
.emoji-menu-toggle-button { .emoji-menu-toggle-button {
@extend .note-action-button; @include emoji-menu-toggle-button;
.no-emoji-placeholder { .no-emoji-placeholder {
position: relative; position: relative;
......
...@@ -830,6 +830,14 @@ ...@@ -830,6 +830,14 @@
} }
} }
.repository-language-bar-tooltip-language {
font-weight: $gl-font-weight-bold;
}
.repository-language-bar-tooltip-share {
color: $theme-gray-400;
}
pre.light-well { pre.light-well {
border-color: $well-light-border; border-color: $well-light-border;
} }
......
@import 'framework/variables'; @import 'framework/variables';
@import 'framework/variables_overrides';
@import 'peek/views/rblineprof'; @import 'peek/views/rblineprof';
#js-peek { #js-peek {
...@@ -6,7 +7,7 @@ ...@@ -6,7 +7,7 @@
left: 0; left: 0;
top: 0; top: 0;
width: 100%; width: 100%;
z-index: 1039; z-index: #{$zindex-modal-backdrop + 1};
height: $performance-bar-height; height: $performance-bar-height;
background: $black; background: $black;
......
...@@ -67,8 +67,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -67,8 +67,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end end
end end
def reset_runners_token def reset_registration_token
@application_setting.reset_runners_registration_token! @application_setting.reset_runners_registration_token!
flash[:notice] = 'New runners registration token has been generated!' flash[:notice] = 'New runners registration token has been generated!'
redirect_to admin_runners_path redirect_to admin_runners_path
end end
......
# frozen_string_literal: true
module LabelsAsHash
extend ActiveSupport::Concern
def labels_as_hash(target = nil, params = {})
available_labels = LabelsFinder.new(
current_user,
params
).execute
label_hashes = available_labels.as_json(only: [:title, :color])
if target&.respond_to?(:labels)
already_set_labels = available_labels & target.labels
if already_set_labels.present?
titles = already_set_labels.map(&:title)
label_hashes.each do |hash|
if titles.include?(hash['title'])
hash[:set] = true
end
end
end
end
label_hashes
end
end
...@@ -12,7 +12,8 @@ class Groups::LabelsController < Groups::ApplicationController ...@@ -12,7 +12,8 @@ class Groups::LabelsController < Groups::ApplicationController
def index def index
respond_to do |format| respond_to do |format|
format.html do format.html do
@labels = GroupLabelsFinder.new(@group, params.merge(sort: sort)).execute @labels = GroupLabelsFinder
.new(current_user, @group, params.merge(sort: sort)).execute
end end
format.json do format.json do
render json: LabelSerializer.new.represent_appearance(available_labels) render json: LabelSerializer.new.represent_appearance(available_labels)
......
...@@ -10,6 +10,13 @@ module Groups ...@@ -10,6 +10,13 @@ module Groups
define_secret_variables define_secret_variables
end end
def reset_registration_token
@group.reset_runners_token!
flash[:notice] = 'New runners registration token has been generated!'
redirect_to group_settings_ci_cd_path
end
private private
def define_secret_variables def define_secret_variables
......
...@@ -163,6 +163,7 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -163,6 +163,7 @@ class Projects::LabelsController < Projects::ApplicationController
project_id: @project.id, project_id: @project.id,
include_ancestor_groups: params[:include_ancestor_groups], include_ancestor_groups: params[:include_ancestor_groups],
search: params[:search], search: params[:search],
subscribed: params[:subscribed],
sort: sort).execute sort: sort).execute
end end
......
...@@ -25,7 +25,13 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic ...@@ -25,7 +25,13 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
@diffs.write_cache @diffs.write_cache
render json: DiffsSerializer.new(current_user: current_user, project: @merge_request.project).represent(@diffs, additional_attributes) request = {
current_user: current_user,
project: @merge_request.project,
render: ->(partial, locals) { view_to_html_string(partial, locals) }
}
render json: DiffsSerializer.new(request).represent(@diffs, additional_attributes)
end end
def define_diff_vars def define_diff_vars
......
...@@ -36,6 +36,13 @@ module Projects ...@@ -36,6 +36,13 @@ module Projects
end end
end end
def reset_registration_token
@project.reset_runners_token!
flash[:notice] = 'New runners registration token has been generated!'
redirect_to namespace_project_settings_ci_cd_path
end
private private
def update_params def update_params
......
...@@ -29,11 +29,17 @@ class UsersController < ApplicationController ...@@ -29,11 +29,17 @@ class UsersController < ApplicationController
format.json do format.json do
load_events load_events
pager_json("events/_events", @events.count) pager_json("events/_events", @events.count, events: @events)
end end
end end
end end
def activity
respond_to do |format|
format.html { render 'show' }
end
end
def groups def groups
load_groups load_groups
...@@ -53,9 +59,7 @@ class UsersController < ApplicationController ...@@ -53,9 +59,7 @@ class UsersController < ApplicationController
respond_to do |format| respond_to do |format|
format.html { render 'show' } format.html { render 'show' }
format.json do format.json do
render json: { pager_json("shared/projects/_list", @projects.count, projects: @projects)
html: view_to_html_string("shared/projects/_list", projects: @projects)
}
end end
end end
end end
...@@ -125,6 +129,7 @@ class UsersController < ApplicationController ...@@ -125,6 +129,7 @@ class UsersController < ApplicationController
@projects = @projects =
PersonalProjectsFinder.new(user).execute(current_user) PersonalProjectsFinder.new(user).execute(current_user)
.page(params[:page]) .page(params[:page])
.per(params[:limit])
prepare_projects_for_rendering(@projects) prepare_projects_for_rendering(@projects)
end end
......
# frozen_string_literal: true # frozen_string_literal: true
class GroupLabelsFinder class GroupLabelsFinder
attr_reader :group, :params attr_reader :current_user, :group, :params
def initialize(group, params = {}) def initialize(current_user, group, params = {})
@current_user = current_user
@group = group @group = group
@params = params @params = params
end end
def execute def execute
group.labels group.labels
.optionally_subscribed_by(subscriber_id)
.optionally_search(params[:search]) .optionally_search(params[:search])
.order_by(params[:sort]) .order_by(params[:sort])
.page(params[:page]) .page(params[:page])
end end
private
def subscriber_id
current_user&.id if subscribed?
end
def subscribed?
params[:subscribed] == 'true'
end
end end
...@@ -17,6 +17,7 @@ class LabelsFinder < UnionFinder ...@@ -17,6 +17,7 @@ class LabelsFinder < UnionFinder
@skip_authorization = skip_authorization @skip_authorization = skip_authorization
items = find_union(label_ids, Label) || Label.none items = find_union(label_ids, Label) || Label.none
items = with_title(items) items = with_title(items)
items = by_subscription(items)
items = by_search(items) items = by_search(items)
sort(items) sort(items)
end end
...@@ -84,6 +85,18 @@ class LabelsFinder < UnionFinder ...@@ -84,6 +85,18 @@ class LabelsFinder < UnionFinder
labels.search(params[:search]) labels.search(params[:search])
end end
def by_subscription(labels)
labels.optionally_subscribed_by(subscriber_id)
end
def subscriber_id
current_user&.id if subscribed?
end
def subscribed?
params[:subscribed] == 'true'
end
# Gets redacted array of group ids # Gets redacted array of group ids
# which can include the ancestors and descendants of the requested group. # which can include the ancestors and descendants of the requested group.
def group_ids_for(group) def group_ids_for(group)
......
...@@ -66,10 +66,6 @@ class MergeRequestsFinder < IssuableFinder ...@@ -66,10 +66,6 @@ class MergeRequestsFinder < IssuableFinder
items.where(target_branch: target_branch) items.where(target_branch: target_branch)
end end
def item_project_ids(items)
items&.reorder(nil)&.select(:target_project_id)
end
def by_wip(items) def by_wip(items)
if params[:wip] == 'yes' if params[:wip] == 'yes'
items.where(wip_match(items.arel_table)) items.where(wip_match(items.arel_table))
......
...@@ -31,7 +31,7 @@ class UserRecentEventsFinder ...@@ -31,7 +31,7 @@ class UserRecentEventsFinder
recent_events(params[:offset] || 0) recent_events(params[:offset] || 0)
.joins(:project) .joins(:project)
.with_associations .with_associations
.limit_recent(LIMIT, params[:offset]) .limit_recent(params[:limit].presence || LIMIT, params[:offset])
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
......
...@@ -59,8 +59,8 @@ module BoardsHelper ...@@ -59,8 +59,8 @@ module BoardsHelper
{ {
toggle: "dropdown", toggle: "dropdown",
list_labels_path: labels_filter_path(true, include_ancestor_groups: true), list_labels_path: labels_filter_path_with_defaults(only_group_labels: true, include_ancestor_groups: true),
labels: labels_filter_path(true, include_descendant_groups: include_descendant_groups), labels: labels_filter_path_with_defaults(only_group_labels: true, include_descendant_groups: include_descendant_groups),
labels_endpoint: @labels_endpoint, labels_endpoint: @labels_endpoint,
namespace_path: @namespace_path, namespace_path: @namespace_path,
project_path: @project&.path, project_path: @project&.path,
......
...@@ -13,8 +13,4 @@ module ClustersHelper ...@@ -13,8 +13,4 @@ module ClustersHelper
render 'projects/clusters/gcp_signup_offer_banner' render 'projects/clusters/gcp_signup_offer_banner'
end end
end end
def rbac_clusters_feature_enabled?
Feature.enabled?(:rbac_clusters)
end
end end
...@@ -131,20 +131,26 @@ module LabelsHelper ...@@ -131,20 +131,26 @@ module LabelsHelper
end end
end end
def labels_filter_path(only_group_labels = false, include_ancestor_groups: true, include_descendant_groups: false) def labels_filter_path_with_defaults(only_group_labels: false, include_ancestor_groups: true, include_descendant_groups: false)
project = @target_project || @project
options = {} options = {}
options[:include_ancestor_groups] = include_ancestor_groups if include_ancestor_groups options[:include_ancestor_groups] = include_ancestor_groups if include_ancestor_groups
options[:include_descendant_groups] = include_descendant_groups if include_descendant_groups options[:include_descendant_groups] = include_descendant_groups if include_descendant_groups
options[:only_group_labels] = only_group_labels if only_group_labels && @group
options[:format] = :json
labels_filter_path(options)
end
def labels_filter_path(options = {})
project = @target_project || @project
format = options.delete(:format) || :html
if project if project
project_labels_path(project, :json, options) project_labels_path(project, format, options)
elsif @group elsif @group
options[:only_group_labels] = only_group_labels if only_group_labels group_labels_path(@group, format, options)
group_labels_path(@group, :json, options)
else else
dashboard_labels_path(:json) dashboard_labels_path(format, options)
end end
end end
......
...@@ -13,6 +13,7 @@ module RepositoryLanguagesHelper ...@@ -13,6 +13,7 @@ module RepositoryLanguagesHelper
content_tag :div, nil, content_tag :div, nil,
class: "progress-bar has-tooltip", class: "progress-bar has-tooltip",
style: "width: #{lang.share}%; background-color:#{lang.color}", style: "width: #{lang.share}%; background-color:#{lang.color}",
title: lang.name data: { html: true },
title: "<span class=\"repository-language-bar-tooltip-language\">#{escape_javascript(lang.name)}</span>&nbsp;<span class=\"repository-language-bar-tooltip-share\">#{lang.share.round(1)}%</span>"
end end
end end
...@@ -76,7 +76,7 @@ module UsersHelper ...@@ -76,7 +76,7 @@ module UsersHelper
tabs = [] tabs = []
if can?(current_user, :read_user_profile, @user) if can?(current_user, :read_user_profile, @user)
tabs += [:activity, :groups, :contributed, :projects, :snippets] tabs += [:overview, :activity, :groups, :contributed, :projects, :snippets]
end end
tabs tabs
......
...@@ -633,6 +633,10 @@ module Ci ...@@ -633,6 +633,10 @@ module Ci
end end
end end
def default_branch?
ref == project.default_branch
end
private private
def ci_yaml_from_repo def ci_yaml_from_repo
......
...@@ -319,7 +319,11 @@ class Commit ...@@ -319,7 +319,11 @@ class Commit
def status(ref = nil) def status(ref = nil)
return @statuses[ref] if @statuses.key?(ref) return @statuses[ref] if @statuses.key?(ref)
@statuses[ref] = project.pipelines.latest_status_per_commit(id, ref)[id] @statuses[ref] = status_for_project(ref, project)
end
def status_for_project(ref, pipeline_project)
pipeline_project.pipelines.latest_status_per_commit(id, ref)[id]
end end
def set_status_for_ref(ref, status) def set_status_for_ref(ref, status)
......
...@@ -31,9 +31,11 @@ module Subscribable ...@@ -31,9 +31,11 @@ module Subscribable
end end
def subscribers(project) def subscribers(project)
subscriptions_available(project) relation = subscriptions_available(project)
.where(subscribed: true) .where(subscribed: true)
.map(&:user) .select(:user_id)
User.where(id: relation)
end end
def toggle_subscription(user, project = nil) def toggle_subscription(user, project = nil)
......
...@@ -64,10 +64,10 @@ class InstanceConfiguration ...@@ -64,10 +64,10 @@ class InstanceConfiguration
end end
def ssh_algorithm_md5(ssh_file_content) def ssh_algorithm_md5(ssh_file_content)
OpenSSL::Digest::MD5.hexdigest(ssh_file_content).scan(/../).join(':') Gitlab::SSHPublicKey.new(ssh_file_content).fingerprint
end end
def ssh_algorithm_sha256(ssh_file_content) def ssh_algorithm_sha256(ssh_file_content)
OpenSSL::Digest::SHA256.hexdigest(ssh_file_content) Gitlab::SSHPublicKey.new(ssh_file_content).fingerprint('SHA256')
end end
end end
...@@ -45,6 +45,7 @@ class Label < ActiveRecord::Base ...@@ -45,6 +45,7 @@ class Label < ActiveRecord::Base
scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) } scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) }
scope :order_name_asc, -> { reorder(title: :asc) } scope :order_name_asc, -> { reorder(title: :asc) }
scope :order_name_desc, -> { reorder(title: :desc) } scope :order_name_desc, -> { reorder(title: :desc) }
scope :subscribed_by, ->(user_id) { joins(:subscriptions).where(subscriptions: { user_id: user_id, subscribed: true }) }
def self.prioritized(project) def self.prioritized(project)
joins(:priorities) joins(:priorities)
...@@ -74,6 +75,14 @@ class Label < ActiveRecord::Base ...@@ -74,6 +75,14 @@ class Label < ActiveRecord::Base
joins(label_priorities) joins(label_priorities)
end end
def self.optionally_subscribed_by(user_id)
if user_id
subscribed_by(user_id)
else
all
end
end
alias_attribute :name, :title alias_attribute :name, :title
def self.reference_prefix def self.reference_prefix
......
...@@ -390,7 +390,11 @@ class ProjectPolicy < BasePolicy ...@@ -390,7 +390,11 @@ class ProjectPolicy < BasePolicy
greedy_load_subject ||= !@user.persisted? greedy_load_subject ||= !@user.persisted?
if greedy_load_subject if greedy_load_subject
project.team.members.include?(user) # We want to load all the members with one query. Calling #include? on
# project.team.members will perform a separate query for each user, unless
# project.team.members was loaded before somewhere else. Calling #to_a
# ensures it's always loaded before checking for membership.
project.team.members.to_a.include?(user)
else else
# otherwise we just make a specific query for # otherwise we just make a specific query for
# this particular user. # this particular user.
......
...@@ -25,4 +25,25 @@ class CommitEntity < API::Entities::Commit ...@@ -25,4 +25,25 @@ class CommitEntity < API::Entities::Commit
expose :title_html, if: { type: :full } do |commit| expose :title_html, if: { type: :full } do |commit|
markdown_field(commit, :title) markdown_field(commit, :title)
end end
expose :signature_html, if: { type: :full } do |commit|
render('projects/commit/_signature', signature: commit.signature) if commit.has_signature?
end
expose :pipeline_status_path, if: { type: :full } do |commit, options|
pipeline_ref = options[:pipeline_ref]
pipeline_project = options[:pipeline_project] || commit.project
next unless pipeline_ref && pipeline_project
status = commit.status_for_project(pipeline_ref, pipeline_project)
next unless status
pipelines_project_commit_path(pipeline_project, commit.id, ref: pipeline_ref)
end
def render(*args)
return unless request.respond_to?(:render) && request.render.respond_to?(:call)
request.render.call(*args)
end
end end
...@@ -10,7 +10,12 @@ class DetailedStatusEntity < Grape::Entity ...@@ -10,7 +10,12 @@ class DetailedStatusEntity < Grape::Entity
expose :illustration do |status| expose :illustration do |status|
begin begin
status.illustration illustration = {
image: ActionController::Base.helpers.image_path(status.illustration[:image])
}
illustration = status.illustration.merge(illustration)
illustration
rescue NotImplementedError rescue NotImplementedError
# ignored # ignored
end end
...@@ -25,5 +30,6 @@ class DetailedStatusEntity < Grape::Entity ...@@ -25,5 +30,6 @@ class DetailedStatusEntity < Grape::Entity
expose :action_title, as: :title expose :action_title, as: :title
expose :action_path, as: :path expose :action_path, as: :path
expose :action_method, as: :method expose :action_method, as: :method
expose :action_button_title, as: :button_title
end end
end end
...@@ -18,7 +18,9 @@ class DiffsEntity < Grape::Entity ...@@ -18,7 +18,9 @@ class DiffsEntity < Grape::Entity
expose :commit do |diffs, options| expose :commit do |diffs, options|
CommitEntity.represent options[:commit], options.merge( CommitEntity.represent options[:commit], options.merge(
type: :full, type: :full,
commit_url_params: { merge_request_iid: merge_request&.iid } commit_url_params: { merge_request_iid: merge_request&.iid },
pipeline_ref: merge_request&.source_branch,
pipeline_project: merge_request&.source_project
) )
end end
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
module Projects module Projects
class AutocompleteService < BaseService class AutocompleteService < BaseService
include LabelsAsHash
def issues def issues
IssuesFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title]) IssuesFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
end end
...@@ -22,34 +23,14 @@ module Projects ...@@ -22,34 +23,14 @@ module Projects
MergeRequestsFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title]) MergeRequestsFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
end end
def labels_as_hash(target = nil)
available_labels = LabelsFinder.new(
current_user,
project_id: project.id,
include_ancestor_groups: true
).execute
label_hashes = available_labels.as_json(only: [:title, :color])
if target&.respond_to?(:labels)
already_set_labels = available_labels & target.labels
if already_set_labels.present?
titles = already_set_labels.map(&:title)
label_hashes.each do |hash|
if titles.include?(hash['title'])
hash[:set] = true
end
end
end
end
label_hashes
end
def commands(noteable, type) def commands(noteable, type)
return [] unless noteable return [] unless noteable
QuickActions::InterpretService.new(project, current_user).available_commands(noteable) QuickActions::InterpretService.new(project, current_user).available_commands(noteable)
end end
def labels_as_hash(target)
super(target, project_id: project.id, include_ancestor_groups: true)
end
end end
end end
...@@ -210,9 +210,14 @@ module QuickActions ...@@ -210,9 +210,14 @@ module QuickActions
end end
params '~label1 ~"label 2"' params '~label1 ~"label 2"'
condition do condition do
available_labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true).execute if project
available_labels = LabelsFinder
.new(current_user, project_id: project.id, include_ancestor_groups: true)
.execute
end
current_user.can?(:"admin_#{issuable.to_ability_name}", project) && project &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
available_labels.any? available_labels.any?
end end
command :label do |labels_param| command :label do |labels_param|
...@@ -286,7 +291,8 @@ module QuickActions ...@@ -286,7 +291,8 @@ module QuickActions
end end
params '#issue | !merge_request' params '#issue | !merge_request'
condition do condition do
current_user.can?(:"update_#{issuable.to_ability_name}", issuable) [MergeRequest, Issue].include?(issuable.class) &&
current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
end end
parse_params do |issuable_param| parse_params do |issuable_param|
extract_references(issuable_param, :issue).first || extract_references(issuable_param, :issue).first ||
...@@ -443,7 +449,8 @@ module QuickActions ...@@ -443,7 +449,8 @@ module QuickActions
end end
params '<time(1h30m | -1h30m)> <date(YYYY-MM-DD)>' params '<time(1h30m | -1h30m)> <date(YYYY-MM-DD)>'
condition do condition do
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) issuable.is_a?(TimeTrackable) &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end end
parse_params do |raw_time_date| parse_params do |raw_time_date|
Gitlab::QuickActions::SpendTimeAndDateSeparator.new(raw_time_date).execute Gitlab::QuickActions::SpendTimeAndDateSeparator.new(raw_time_date).execute
...@@ -493,7 +500,7 @@ module QuickActions ...@@ -493,7 +500,7 @@ module QuickActions
desc "Lock the discussion" desc "Lock the discussion"
explanation "Locks the discussion" explanation "Locks the discussion"
condition do condition do
issuable.is_a?(Issuable) && [MergeRequest, Issue].include?(issuable.class) &&
issuable.persisted? && issuable.persisted? &&
!issuable.discussion_locked? && !issuable.discussion_locked? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
...@@ -505,7 +512,7 @@ module QuickActions ...@@ -505,7 +512,7 @@ module QuickActions
desc "Unlock the discussion" desc "Unlock the discussion"
explanation "Unlocks the discussion" explanation "Unlocks the discussion"
condition do condition do
issuable.is_a?(Issuable) && [MergeRequest, Issue].include?(issuable.class) &&
issuable.persisted? && issuable.persisted? &&
issuable.discussion_locked? && issuable.discussion_locked? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
......
- add_to_breadcrumbs "Projects", admin_projects_path - add_to_breadcrumbs "Projects", admin_projects_path
- breadcrumb_title @project.full_name - breadcrumb_title @project.full_name
- page_title @project.full_name, "Projects" - page_title @project.full_name, "Projects"
- @content_class = "admin-projects"
%h3.page-title %h3.page-title
Project: #{@project.full_name} Project: #{@project.full_name}
= link_to edit_project_path(@project), class: "btn btn-nr float-right" do = link_to edit_project_path(@project), class: "btn btn-nr float-right" do
......
...@@ -2,106 +2,103 @@ ...@@ -2,106 +2,103 @@
- @no_container = true - @no_container = true
%div{ class: container_class } %div{ class: container_class }
.bs-callout .row
%p .col-sm-6
= (_"A 'Runner' is a process which runs a job. You can set up as many Runners as you need.") .bs-callout
%br %p
= _('Runners can be placed on separate users, servers, even on your local machine.') = (_"A 'Runner' is a process which runs a job. You can set up as many Runners as you need.")
%br %br
= _('Runners can be placed on separate users, servers, even on your local machine.')
%br
%div %div
%span= _('Each Runner can be in one of the following states:') %span= _('Each Runner can be in one of the following states:')
%ul %ul
%li %li
%span.badge.badge-success shared %span.badge.badge-success shared
\- \-
= _('Runner runs jobs from all unassigned projects') = _('Runner runs jobs from all unassigned projects')
%li %li
%span.badge.badge-success group %span.badge.badge-success group
\- \-
= _('Runner runs jobs from all unassigned projects in its group') = _('Runner runs jobs from all unassigned projects in its group')
%li %li
%span.badge.badge-info specific %span.badge.badge-info specific
\- \-
= _('Runner runs jobs from assigned projects') = _('Runner runs jobs from assigned projects')
%li %li
%span.badge.badge-warning locked %span.badge.badge-warning locked
\- \-
= _('Runner cannot be assigned to other projects') = _('Runner cannot be assigned to other projects')
%li %li
%span.badge.badge-danger paused %span.badge.badge-danger paused
\- \-
= _('Runner will not receive any new jobs') = _('Runner will not receive any new jobs')
.bs-callout.clearfix .col-sm-6
.float-left .bs-callout
%p = render partial: 'ci/runner/how_to_setup_runner',
= _('You can reset runners registration token by pressing a button below.') locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token,
.prepend-top-10 type: 'shared',
= button_to _('Reset runners registration token'), reset_runners_token_admin_application_settings_path, reset_token_url: reset_registration_token_admin_application_settings_path }
method: :put, class: 'btn btn-default',
data: { confirm: _('Are you sure you want to reset registration token?') }
= render partial: 'ci/runner/how_to_setup_shared_runner', .row
locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token } .col-sm-9
= form_tag admin_runners_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do
.filtered-search-wrapper
.filtered-search-box
= dropdown_tag(custom_icon('icon_history'),
options: { wrapper_class: 'filtered-search-history-dropdown-wrapper',
toggle_class: 'filtered-search-history-dropdown-toggle-button',
dropdown_class: 'filtered-search-history-dropdown',
content_class: 'filtered-search-history-dropdown-content',
title: _('Recent searches') }) do
.js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } }
.filtered-search-box-input-container.droplab-dropdown
.scroll-container
%ul.tokens-container.list-unstyled
%li.input-token
%input.form-control.filtered-search{ { id: 'filtered-search-runners', placeholder: _('Search or filter results...') } }
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
= button_tag class: %w[btn btn-link] do
= sprite_icon('search')
%span
= _('Press Enter or click to search')
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
= button_tag class: %w[btn btn-link] do
-# Encapsulate static class name `{{icon}}` inside #{} to bypass
-# haml lint's ClassAttributeWithStaticValue
%svg
%use{ 'xlink:href': "#{'{{icon}}'}" }
%span.js-filter-hint
{{hint}}
%span.js-filter-tag.dropdown-light-content
{{tag}}
.bs-callout #js-dropdown-admin-runner-status.filtered-search-input-dropdown-menu.dropdown-menu
%p %ul{ data: { dropdown: true } }
= _('Runners currently online: %{active_runners_count}') % { active_runners_count: @active_runners_count } - Ci::Runner::AVAILABLE_STATUSES.each do |status|
%li.filter-dropdown-item{ data: { value: status } }
= button_tag class: %w[btn btn-link] do
= status.titleize
.row-content-block.second-block #js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu
= form_tag admin_runners_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do %ul{ data: { dropdown: true } }
.filtered-search-wrapper - Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
.filtered-search-box %li.filter-dropdown-item{ data: { value: runner_type } }
= dropdown_tag(custom_icon('icon_history'), = button_tag class: %w[btn btn-link] do
options: { wrapper_class: 'filtered-search-history-dropdown-wrapper', = runner_type.titleize
toggle_class: 'filtered-search-history-dropdown-toggle-button',
dropdown_class: 'filtered-search-history-dropdown',
content_class: 'filtered-search-history-dropdown-content',
title: _('Recent searches') }) do
.js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } }
.filtered-search-box-input-container.droplab-dropdown
.scroll-container
%ul.tokens-container.list-unstyled
%li.input-token
%input.form-control.filtered-search{ { id: 'filtered-search-runners', placeholder: _('Search or filter results...') } }
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
= button_tag class: %w[btn btn-link] do
= sprite_icon('search')
%span
= _('Press Enter or click to search')
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
= button_tag class: %w[btn btn-link] do
-# Encapsulate static class name `{{icon}}` inside #{} to bypass
-# haml lint's ClassAttributeWithStaticValue
%svg
%use{ 'xlink:href': "#{'{{icon}}'}" }
%span.js-filter-hint
{{hint}}
%span.js-filter-tag.dropdown-light-content
{{tag}}
#js-dropdown-admin-runner-status.filtered-search-input-dropdown-menu.dropdown-menu = button_tag class: %w[clear-search hidden] do
%ul{ data: { dropdown: true } } = icon('times')
- Ci::Runner::AVAILABLE_STATUSES.each do |status| .filter-dropdown-container
%li.filter-dropdown-item{ data: { value: status } } = render 'sort_dropdown'
= button_tag class: %w[btn btn-link] do
= status.titleize
#js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu .col-sm-3.text-right-lg
%ul{ data: { dropdown: true } } = _('Runners currently online: %{active_runners_count}') % { active_runners_count: @active_runners_count }
- Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
%li.filter-dropdown-item{ data: { value: runner_type } }
= button_tag class: %w[btn btn-link] do
= runner_type.titleize
= button_tag class: %w[clear-search hidden] do
= icon('times')
.filter-dropdown-container
= render 'sort_dropdown'
- if @runners.any? - if @runners.any?
.runners-content.content-list .runners-content.content-list
......
...@@ -13,5 +13,9 @@ ...@@ -13,5 +13,9 @@
= _("Use the following registration token during setup:") = _("Use the following registration token during setup:")
%code#registration_token= registration_token %code#registration_token= registration_token
= clipboard_button(target: '#registration_token', title: _("Copy token to clipboard"), class: "btn-transparent btn-clipboard") = clipboard_button(target: '#registration_token', title: _("Copy token to clipboard"), class: "btn-transparent btn-clipboard")
.prepend-top-10.append-bottom-10
= button_to _("Reset runners registration token"), reset_token_url,
method: :put, class: 'btn btn-default',
data: { confirm: _("Are you sure you want to reset registration token?") }
%li %li
= _("Start the Runner!") = _("Start the Runner!")
.bs-callout.help-callout
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: registration_token, type: 'shared' }
.bs-callout.help-callout
.append-bottom-10
%h4= _('Set up a specific Runner automatically')
%p
- link_to_help_page = link_to(_('Learn more about Kubernetes'),
help_page_path('user/project/clusters/index'),
target: '_blank',
rel: 'noopener noreferrer')
= _('You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page }
%ol
%li
= _('Click the button below to begin the install process by navigating to the Kubernetes page')
%li
= _('Select an existing Kubernetes cluster or create a new one')
%li
= _('From the Kubernetes cluster details view, install Runner from the applications list')
= link_to _('Install Runner on Kubernetes'),
project_clusters_path(@project),
class: 'btn btn-info'
%hr
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: registration_token, type: 'specific' }
...@@ -3,29 +3,23 @@ ...@@ -3,29 +3,23 @@
- can_admin_label = can?(current_user, :admin_label, @group) - can_admin_label = can?(current_user, :admin_label, @group)
- issuables = ['issues', 'merge requests'] - issuables = ['issues', 'merge requests']
- search = params[:search] - search = params[:search]
- subscribed = params[:subscribed]
- labels_or_filters = @labels.exists? || search.present? || subscribed.present?
- if can_admin_label - if can_admin_label
- content_for(:header_content) do - content_for(:header_content) do
.nav-controls .nav-controls
= link_to _('New label'), new_group_label_path(@group), class: "btn btn-success" = link_to _('New label'), new_group_label_path(@group), class: "btn btn-success"
- if @labels.exists? || search.present? - if labels_or_filters
#promote-label-modal #promote-label-modal
%div{ class: container_class } %div{ class: container_class }
.top-area.adjust = render 'shared/labels/nav'
.nav-text
= _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuables.to_sentence }
.nav-controls
= form_tag group_labels_path(@group), method: :get do
.input-group
= search_field_tag :search, params[:search], { placeholder: _('Filter'), id: 'label-search', class: 'form-control search-text-input input-short', spellcheck: false }
%span.input-group-append
%button.btn.btn-default{ type: "submit", "aria-label" => _('Submit search') }
= icon("search")
= render 'shared/labels/sort_dropdown'
.labels-container.prepend-top-5 .labels-container.prepend-top-5
- if @labels.any? - if @labels.any?
.text-muted
= _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuables.to_sentence }
.other-labels .other-labels
%h5= _('Labels') %h5= _('Labels')
%ul.content-list.manage-labels-list.js-other-labels %ul.content-list.manage-labels-list.js-other-labels
...@@ -34,6 +28,9 @@ ...@@ -34,6 +28,9 @@
- elsif search.present? - elsif search.present?
.nothing-here-block .nothing-here-block
= _('No labels with such name or description') = _('No labels with such name or description')
- elsif subscribed.present?
.nothing-here-block
= _('You do not have any subscriptions yet')
- else - else
= render 'shared/empty_states/labels' = render 'shared/empty_states/labels'
......
...@@ -11,7 +11,9 @@ ...@@ -11,7 +11,9 @@
-# https://gitlab.com/gitlab-org/gitlab-ce/issues/45894 -# https://gitlab.com/gitlab-org/gitlab-ce/issues/45894
- if can?(current_user, :admin_pipeline, @group) - if can?(current_user, :admin_pipeline, @group)
= render partial: 'ci/runner/how_to_setup_runner', = render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: @group.runners_token, type: 'group' } locals: { registration_token: @group.runners_token,
type: 'group',
reset_token_url: reset_registration_token_group_settings_ci_cd_path }
- if @group.runners.empty? - if @group.runners.empty?
%h4.underlined-title %h4.underlined-title
......
...@@ -5,7 +5,14 @@ ...@@ -5,7 +5,14 @@
.user-name.bold .user-name.bold
= current_user.name = current_user.name
= current_user.to_reference = current_user.to_reference
- if current_user.status
.user-status-emoji.str-truncated.has-tooltip{ title: current_user.status.message_html, data: { html: 'true', placement: 'bottom' } }
= emoji_icon current_user.status.emoji
= current_user.status.message_html.html_safe
%li.divider %li.divider
- if can?(current_user, :update_user_status, current_user)
%li
.js-set-status-modal-trigger{ data: { has_status: current_user.status.present? ? 'true' : 'false' } }
- if current_user_menu?(:profile) - if current_user_menu?(:profile)
%li %li
= link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username } = link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username }
......
...@@ -74,3 +74,6 @@ ...@@ -74,3 +74,6 @@
%span.sr-only= _('Toggle navigation') %span.sr-only= _('Toggle navigation')
= sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right') = sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right')
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left') = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
- if can?(current_user, :update_user_status, current_user)
.js-set-status-modal-wrapper{ data: { current_emoji: current_user.status.present? ? current_user.status.emoji : '', current_message: current_user.status.present? ? current_user.status.message : '' } }
%p.slead %p.slead
Should you ever lose your phone, each of these recovery codes can be used one Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one
time each to regain access to your account. Please save them in a safe place, or you time each to regain access to your account. Please save them in a safe place, or you
%b will %b will
lose access to your account. lose access to your account.
......
...@@ -6,13 +6,13 @@ ...@@ -6,13 +6,13 @@
.row.prepend-top-default .row.prepend-top-default
.col-lg-4 .col-lg-4
%h4.prepend-top-0 %h4.prepend-top-0
Register Two-Factor Authentication App Register Two-Factor Authenticator
%p %p
Use an app on your mobile device to enable two-factor authentication (2FA). Use an one time password authenticator on your mobile device or computer to enable two-factor authentication (2FA).
.col-lg-8 .col-lg-8
- if current_user.two_factor_otp_enabled? - if current_user.two_factor_otp_enabled?
%p %p
You've already enabled two-factor authentication using mobile authenticator applications. In order to register a different device, you must first disable two-factor authentication. You've already enabled two-factor authentication using one time password authenticators. In order to register a different device, you must first disable two-factor authentication.
%p %p
If you lose your recovery codes you can generate new ones, invalidating all previous codes. If you lose your recovery codes you can generate new ones, invalidating all previous codes.
%div %div
......
...@@ -61,15 +61,14 @@ ...@@ -61,15 +61,14 @@
%p.form-text.text-muted %p.form-text.text-muted
= s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end } = s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end }
- if rbac_clusters_feature_enabled? .form-group
.form-group .form-check
.form-check = provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true
= provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
= provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' .form-text.text-muted
.form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank'
= link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank'
.form-group .form-group
= field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true
...@@ -37,14 +37,13 @@ ...@@ -37,14 +37,13 @@
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
- if rbac_clusters_feature_enabled? .form-group
.form-group .form-check
.form-check = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
= platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' .form-text.text-muted
.form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
.form-group .form-group
= field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success' = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
...@@ -25,15 +25,14 @@ ...@@ -25,15 +25,14 @@
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold' = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
- if rbac_clusters_feature_enabled? .form-group
.form-group .form-check
.form-check = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input qa-rbac-checkbox' }, 'rbac', 'abac'
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input' }, 'rbac', 'abac' = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
= platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' .form-text.text-muted
.form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank'
= link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank'
.form-group .form-group
= field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success' = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success'
...@@ -26,14 +26,13 @@ ...@@ -26,14 +26,13 @@
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold' = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
- if rbac_clusters_feature_enabled? .form-group
.form-group .form-check
.form-check = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
= platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' .form-text.text-muted
.form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
.form-group .form-group
= field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success' = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
...@@ -49,15 +49,16 @@ ...@@ -49,15 +49,16 @@
.environments-container .environments-container
- if @deployments.blank? - if @deployments.blank?
.blank-state-row .empty-state
.blank-state-center .text-content
%h2.blank-state-title %h4.state-title
You don't have any deployments right now. You don't have any deployments right now.
%p.blank-state-text %p.blank-state-text
Define environments in the deploy stage(s) in Define environments in the deploy stage(s) in
%code .gitlab-ci.yml %code .gitlab-ci.yml
to track deployments here. to track deployments here.
= link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success" .text-center
= link_to _("Read more"), help_page_path("ci/environments"), class: "btn btn-success"
- else - else
.table-holder .table-holder
.ci-table.environments{ role: 'grid' } .ci-table.environments{ role: 'grid' }
......
- illustration = local_assigns.fetch(:illustration)
- illustration_size = local_assigns.fetch(:illustration_size)
- title = local_assigns.fetch(:title)
- content = local_assigns.fetch(:content, nil)
- action = local_assigns.fetch(:action, nil)
.row.empty-state
.col-12
.svg-content{ class: illustration_size }
= image_tag illustration
.col-12
.text-content
%h4.text-center= title
- if content
%p= content
- if action
.text-center
= action
- detailed_status = @build.detailed_status(current_user)
- illustration = detailed_status.illustration
= render 'empty_state',
illustration: illustration[:image],
illustration_size: illustration[:size],
title: illustration[:title],
content: illustration[:content],
action: detailed_status.has_action? ? link_to(detailed_status.action_button_title, detailed_status.action_path, method: detailed_status.action_method, class: 'btn btn-primary', title: detailed_status.action_button_title) : nil
...@@ -42,8 +42,6 @@ ...@@ -42,8 +42,6 @@
= custom_icon('scroll_down') = custom_icon('scroll_down')
= render 'shared/builds/build_output' = render 'shared/builds/build_output'
- else
= render "empty_states"
#js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } } #js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } }
......
...@@ -2,32 +2,25 @@ ...@@ -2,32 +2,25 @@
- page_title "Labels" - page_title "Labels"
- can_admin_label = can?(current_user, :admin_label, @project) - can_admin_label = can?(current_user, :admin_label, @project)
- search = params[:search] - search = params[:search]
- subscribed = params[:subscribed]
- labels_or_filters = @labels.exists? || @prioritized_labels.exists? || search.present? || subscribed.present?
- if can_admin_label - if can_admin_label
- content_for(:header_content) do - content_for(:header_content) do
.nav-controls .nav-controls
= link_to _('New label'), new_project_label_path(@project), class: "btn btn-success" = link_to _('New label'), new_project_label_path(@project), class: "btn btn-success"
- if @labels.exists? || @prioritized_labels.exists? || search.present? - if labels_or_filters
#promote-label-modal #promote-label-modal
%div{ class: container_class } %div{ class: container_class }
.top-area.adjust = render 'shared/labels/nav'
.nav-text
= _('Labels can be applied to issues and merge requests.')
.nav-controls
= form_tag project_labels_path(@project), method: :get do
.input-group
= search_field_tag :search, params[:search], { placeholder: _('Filter'), id: 'label-search', class: 'form-control search-text-input input-short', spellcheck: false }
%span.input-group-append
%button.btn.btn-default{ type: "submit", "aria-label" => _('Submit search') }
= icon("search")
= render 'shared/labels/sort_dropdown'
.labels-container.prepend-top-10 .labels-container.prepend-top-10
- if can_admin_label - if can_admin_label
- if search.blank? - if search.blank?
%p.text-muted %p.text-muted
= _('Labels can be applied to issues and merge requests.')
%br
= _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.') = _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.')
-# Only show it in the first page -# Only show it in the first page
- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
...@@ -59,7 +52,9 @@ ...@@ -59,7 +52,9 @@
- else - else
.nothing-here-block .nothing-here-block
= _('No labels with such name or description') = _('No labels with such name or description')
- elsif subscribed.present?
.nothing-here-block
= _('You do not have any subscriptions yet')
- else - else
= render 'shared/empty_states/labels' = render 'shared/empty_states/labels'
......
%h3 %h3
= _('Specific Runners') = _('Specific Runners')
= render partial: 'ci/runner/how_to_setup_specific_runner', .bs-callout.help-callout
locals: { registration_token: @project.runners_token } .append-bottom-10
%h4= _('Set up a specific Runner automatically')
%p
- link_to_help_page = link_to(_('Learn more about Kubernetes'),
help_page_path('user/project/clusters/index'),
target: '_blank',
rel: 'noopener noreferrer')
= _('You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page }
%ol
%li
= _('Click the button below to begin the install process by navigating to the Kubernetes page')
%li
= _('Select an existing Kubernetes cluster or create a new one')
%li
= _('From the Kubernetes cluster details view, install Runner from the applications list')
= link_to _('Install Runner on Kubernetes'),
project_clusters_path(@project),
class: 'btn btn-info'
%hr
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: @project.runners_token,
type: 'specific',
reset_token_url: reset_registration_token_namespace_project_settings_ci_cd_path }
- if @project_runners.any? - if @project_runners.any?
%h4.underlined-title Runners activated for this project %h4.underlined-title Runners activated for this project
......
...@@ -3,16 +3,6 @@ ...@@ -3,16 +3,6 @@
= form_for @project, url: project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings') do |f| = form_for @project, url: project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings') do |f|
= form_errors(@project) = form_errors(@project)
%fieldset.builds-feature %fieldset.builds-feature
.form-group.append-bottom-default.js-secret-runner-token
= f.label :runners_token, _("Runner token"), class: 'label-bold'
.form-control.js-secret-value-placeholder
= '*' * 20
= f.text_field :runners_token, class: "form-control hide js-secret-value", placeholder: 'xEeFCaDAB89'
%p.form-text.text-muted= _("The secure token used by the Runner to checkout the project")
%button.btn.btn-info.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: 'false' } }
= _('Reveal value')
%hr
.form-group .form-group
%h5.prepend-top-0 %h5.prepend-top-0
= _("Git strategy for pipelines") = _("Git strategy for pipelines")
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
%button.btn.js-settings-toggle{ type: 'button' } %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand') = expanded ? _('Collapse') : _('Expand')
%p %p
= _("Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.") = _("Customize your pipeline configuration, view your pipeline status and coverage report.")
.settings-content .settings-content
= render 'form' = render 'form'
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
- if @project - if @project
%board-add-issues-modal{ "new-issue-path" => new_project_issue_path(@project), %board-add-issues-modal{ "new-issue-path" => new_project_issue_path(@project),
"milestone-path" => milestones_filter_dropdown_path, "milestone-path" => milestones_filter_dropdown_path,
"label-path" => labels_filter_path, "label-path" => labels_filter_path_with_defaults,
"empty-state-svg" => image_path('illustrations/issues.svg'), "empty-state-svg" => image_path('illustrations/issues.svg'),
":issue-link-base" => "issueLinkBase", ":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath", ":root-path" => "rootPath",
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
show_no: "true", show_no: "true",
show_any: "true", show_any: "true",
project_id: @project&.try(:id), project_id: @project&.try(:id),
labels: labels_filter_path(false), labels: labels_filter_path_with_defaults,
namespace_path: @namespace_path, namespace_path: @namespace_path,
project_path: @project.try(:path) } } project_path: @project.try(:path) } }
%span.dropdown-toggle-text %span.dropdown-toggle-text
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
- classes = local_assigns.fetch(:classes, []) - classes = local_assigns.fetch(:classes, [])
- selected = local_assigns.fetch(:selected, nil) - selected = local_assigns.fetch(:selected, nil)
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label") - dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label")
- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"} - dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), labels: labels_filter_path_with_defaults, default_label: "Labels"}
- dropdown_data.merge!(data_options) - dropdown_data.merge!(data_options)
- label_name = local_assigns.fetch(:label_name, "Labels") - label_name = local_assigns.fetch(:label_name, "Labels")
- no_default_styles = local_assigns.fetch(:no_default_styles, false) - no_default_styles = local_assigns.fetch(:no_default_styles, false)
......
...@@ -109,7 +109,7 @@ ...@@ -109,7 +109,7 @@
- selected_labels.each do |label| - selected_labels.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown .dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path(false) if @project), display: 'static' } } %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path_with_defaults if @project), display: 'static' } }
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels") = multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true') = icon('chevron-down', 'aria-hidden': 'true')
......
- subscribed = params[:subscribed]
.top-area.adjust
%ul.nav-links.nav.nav-tabs
%li{ class: active_when(subscribed != 'true') }>
= link_to labels_filter_path do
= _('All')
- if current_user
%li{ class: active_when(subscribed == 'true') }>
= link_to labels_filter_path(subscribed: 'true') do
= _('Subscribed')
.nav-controls
= form_tag labels_filter_path, method: :get do
= hidden_field_tag :subscribed, params[:subscribed]
.input-group
= search_field_tag :search, params[:search], { placeholder: _('Filter'), id: 'label-search', class: 'form-control search-text-input input-short', spellcheck: false }
%span.input-group-append
%button.btn.btn-default{ type: "submit", "aria-label" => _('Submit search') }
= icon("search")
= render 'shared/labels/sort_dropdown'
...@@ -6,4 +6,4 @@ ...@@ -6,4 +6,4 @@
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-sort %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-sort
%li %li
- label_sort_options_hash.each do |value, title| - label_sort_options_hash.each do |value, title|
= sortable_item(title, page_filter_path(sort: value, label: true), sort_title) = sortable_item(title, page_filter_path(sort: value, label: true, subscribed: params[:subscribed]), sort_title)
.row
.col-md-12.col-lg-6
.calendar-block
.content-block.hide-bottom-border
%h4
= s_('UserProfile|Activity')
.user-calendar.d-none.d-sm-block.text-left{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } }
%h4.center.light
%i.fa.fa-spinner.fa-spin
.user-calendar-activities.d-none.d-sm-block
- if can?(current_user, :read_cross_project)
.activities-block
.content-block
%h5.prepend-top-10
= s_('UserProfile|Recent contributions')
.overview-content-list{ data: { href: user_path } }
.center.light.loading
%i.fa.fa-spinner.fa-spin
.prepend-top-10
= link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
.col-md-12.col-lg-6
.projects-block
.content-block
%h4
= s_('UserProfile|Personal projects')
.overview-content-list{ data: { href: user_projects_path } }
.center.light.loading
%i.fa.fa-spinner.fa-spin
.prepend-top-10
= link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
...@@ -12,22 +12,22 @@ ...@@ -12,22 +12,22 @@
.cover-block.user-cover-block.top-area .cover-block.user-cover-block.top-area
.cover-controls .cover-controls
- if @user == current_user - if @user == current_user
= link_to profile_path, class: 'btn btn-default has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do = link_to profile_path, class: 'btn btn-default has-tooltip', title: s_('UserProfile|Edit profile'), 'aria-label': 'Edit profile' do
= icon('pencil') = icon('pencil')
- elsif current_user - elsif current_user
- if @user.abuse_report - if @user.abuse_report
%button.btn.btn-danger{ title: 'Already reported for abuse', %button.btn.btn-danger{ title: s_('UserProfile|Already reported for abuse'),
data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } } data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } }
= icon('exclamation-circle') = icon('exclamation-circle')
- else - else
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn', = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn',
title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do title: s_('UserProfile|Report abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('exclamation-circle') = icon('exclamation-circle')
- if can?(current_user, :read_user_profile, @user) - if can?(current_user, :read_user_profile, @user)
= link_to user_path(@user, rss_url_options), class: 'btn btn-default has-tooltip', title: 'Subscribe', 'aria-label': 'Subscribe' do = link_to user_path(@user, rss_url_options), class: 'btn btn-default has-tooltip', title: s_('UserProfile|Subscribe'), 'aria-label': 'Subscribe' do
= icon('rss') = icon('rss')
- if current_user && current_user.admin? - if current_user && current_user.admin?
= link_to [:admin, @user], class: 'btn btn-default', title: 'View user in admin area', = link_to [:admin, @user], class: 'btn btn-default', title: s_('UserProfile|View user in admin area'),
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('users') = icon('users')
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
@#{@user.username} @#{@user.username}
- if can?(current_user, :read_user_profile, @user) - if can?(current_user, :read_user_profile, @user)
%span.middle-dot-divider %span.middle-dot-divider
Member since #{@user.created_at.to_date.to_s(:long)} = s_('Member since %{date}') % { date: @user.created_at.to_date.to_s(:long) }
.cover-desc .cover-desc
- unless @user.public_email.blank? - unless @user.public_email.blank?
...@@ -91,32 +91,40 @@ ...@@ -91,32 +91,40 @@
.fade-left= icon('angle-left') .fade-left= icon('angle-left')
.fade-right= icon('angle-right') .fade-right= icon('angle-right')
%ul.nav-links.user-profile-nav.scrolling-tabs.nav.nav-tabs %ul.nav-links.user-profile-nav.scrolling-tabs.nav.nav-tabs
- if profile_tab?(:overview)
%li.js-overview-tab
= link_to user_path, data: { target: 'div#js-overview', action: 'overview', toggle: 'tab' } do
= s_('UserProfile|Overview')
- if profile_tab?(:activity) - if profile_tab?(:activity)
%li.js-activity-tab %li.js-activity-tab
= link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do = link_to user_activity_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
Activity = s_('UserProfile|Activity')
- if profile_tab?(:groups) - if profile_tab?(:groups)
%li.js-groups-tab %li.js-groups-tab
= link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do = link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
Groups = s_('UserProfile|Groups')
- if profile_tab?(:contributed) - if profile_tab?(:contributed)
%li.js-contributed-tab %li.js-contributed-tab
= link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do = link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do
Contributed projects = s_('UserProfile|Contributed projects')
- if profile_tab?(:projects) - if profile_tab?(:projects)
%li.js-projects-tab %li.js-projects-tab
= link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do = link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do
Personal projects = s_('UserProfile|Personal projects')
- if profile_tab?(:snippets) - if profile_tab?(:snippets)
%li.js-snippets-tab %li.js-snippets-tab
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do = link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
Snippets = s_('UserProfile|Snippets')
%div{ class: container_class } %div{ class: container_class }
.tab-content .tab-content
- if profile_tab?(:overview)
#js-overview.tab-pane
= render "users/overview"
- if profile_tab?(:activity) - if profile_tab?(:activity)
#activity.tab-pane #activity.tab-pane
.row-content-block.calender-block.white.second-block.d-none.d-sm-block .row-content-block.calendar-block.white.second-block.d-none.d-sm-block
.user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } } .user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } }
%h4.center.light %h4.center.light
%i.fa.fa-spinner.fa-spin %i.fa.fa-spinner.fa-spin
...@@ -124,7 +132,7 @@ ...@@ -124,7 +132,7 @@
- if can?(current_user, :read_cross_project) - if can?(current_user, :read_cross_project)
%h4.prepend-top-20 %h4.prepend-top-20
Most Recent Activity = s_('UserProfile|Most Recent Activity')
.content_list{ data: { href: user_path } } .content_list{ data: { href: user_path } }
= spinner = spinner
...@@ -155,4 +163,4 @@ ...@@ -155,4 +163,4 @@
.col-12.text-center .col-12.text-center
.text-content .text-content
%h4 %h4
This user has a private profile = s_('UserProfile|This user has a private profile')
---
title: Show percentage of language detection on the language bar
merge_request: 22056
author: Johann Hubert Sonntagbauer
type: added
---
title: "Fix the issue where long environment names aren't being truncated, causing the environment name to overlap into the column next to it."
merge_request: 22104
type: fixed
---
title: Rephrase 2FA and TOTP documentation and view
merge_request: 21998
author: Marc Schwede
type: other
---
title: Instance Configuration page now displays correct SSH fingerprints
merge_request: 22081
author:
type: fixed
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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