Merge remote-tracking branch 'origin/master' into 34141-allow-unauthenticated-access-to-the-users-api

- Modify policy code to work with the `DeclarativePolicy` refactor
  in 37c401433b.
This commit is contained in:
Timothy Andrew 2017-06-30 13:29:34 +00:00
commit 5dedea358d
844 changed files with 20562 additions and 7991 deletions

1
.gitignore vendored
View file

@ -21,6 +21,7 @@ eslint-report.html
/.yarn-cache
/.byebug_history
/Vagrantfile
/app/assets/javascripts/locale/**/app.js
/backups/*
/config/aws.yml
/config/database.yml

View file

@ -63,7 +63,7 @@ stages:
.only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql
only:
- /mysql/
- /-stable$/
- /-stable/
- master@gitlab-org/gitlab-ce
- master@gitlab/gitlabhq
- tags@gitlab-org/gitlab-ce
@ -193,6 +193,7 @@ setup-test-env:
script:
- node --version
- yarn install --pure-lockfile --cache-folder .yarn-cache
- bundle exec rake gettext:po_to_json
- bundle exec rake gitlab:assets:compile
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
artifacts:
@ -433,6 +434,7 @@ gitlab:assets:compile:
NO_COMPRESSION: "true"
script:
- yarn install --pure-lockfile --production --cache-folder .yarn-cache
- bundle exec rake gettext:po_to_json
- bundle exec rake gitlab:assets:compile
artifacts:
name: webpack-report
@ -450,6 +452,7 @@ karma:
BABEL_ENV: "coverage"
CHROME_LOG_FILE: "chrome_debug.log"
script:
- bundle exec rake gettext:po_to_json
- bundle exec rake karma
coverage: '/^Statements *: (\d+\.\d+%)/'
artifacts:
@ -471,8 +474,10 @@ codeclimate:
services:
- docker:dind
script:
- docker pull stedolan/jq
- docker pull codeclimate/codeclimate
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json
- cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
artifacts:
paths: [codeclimate.json]
@ -547,3 +552,9 @@ cache gems:
only:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
gitlab_git_test:
variables:
SETUP_DB: "false"
script:
- spec/support/prepare-gitlab-git-test-for-commit --check-for-changes

View file

@ -2,6 +2,234 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 9.3.2 (2017-06-27)
- API: Fix optional arugments for POST :id/variables. !12474
- Bump premailer-rails gem to 1.9.7 and its dependencies to prevent network retrieval of assets.
## 9.3.1 (2017-06-26)
- Fix reversed breadcrumb order for nested groups. !12322
- Fix 500 when failing to create private group. !12394
- Fix linking to line number on side-by-side diff creating empty discussion box.
- Don't match tilde and exclamation mark as part of requirements.txt package name.
- Perform project housekeeping after importing projects.
- Fixed ctrl+enter not submit issue edit form.
## 9.3.0 (2017-06-22)
- Refactored gitlab:app:check into SystemCheck liberary and improve some checks. !9173
- Add an ability to cancel attaching file and redesign attaching files UI. !9431 (blackst0ne)
- Add Aliyun OSS as the backup storage provider. !9721 (Yuanfei Zhu)
- Add suport for find_local_branches GRPC from Gitaly. !10059
- Allow manual bypass of auto_sign_in_with_provider with a new param. !10187 (Maxime Besson)
- Redirect to user's keys index instead of user's index after a key is deleted in the admin. !10227 (Cyril Jouve)
- Changed Blame to Annotate in the UI to promote blameless culture. !10378 (Ilya Vassilevsky)
- Implement ability to update deploy keys. !10383 (Alexander Randa)
- Allow numeric values in gitlab-ci.yml. !10607 (blackst0ne)
- Add a feature test for Unicode trace. !10736 (dosuken123)
- Notes: Warning message should go away once resolved. !10823 (Jacopo Beschi @jacopo-beschi)
- Project authorizations are calculated much faster when using PostgreSQL, and nested groups support for MySQL has been removed
. !10885
- Fix long urls in the title of commit. !10938 (Alexander Randa)
- Update gem sidekiq-cron from 0.4.4 to 0.6.0 and rufus-scheduler from 3.1.10 to 3.4.0. !10976 (dosuken123)
- Use relative paths for group/project/user avatars. !11001 (blackst0ne)
- Enable cancelling non-HEAD pending pipelines by default for all projects. !11023
- Implement web hook logging. !11027 (Alexander Randa)
- Add indices for auto_canceled_by_id for ci_pipelines and ci_builds on PostgreSQL. !11034
- Add post-deploy migration to clean up projects in `pending_delete` state. !11044
- Limit User's trackable attributes, like `current_sign_in_at`, to update at most once/hour. !11053
- Disallow multiple selections for Milestone dropdown. !11084
- Link to commit author user page from pipelines. !11100
- Fix the last coverage in trace log should be extracted. !11128 (dosuken123)
- Remove redirect for old issue url containing id instead of iid. !11135 (blackst0ne)
- Backported new SystemHook event: `repository_update`. !11140
- Keep input data after creating a tag that already exists. !11155
- Fix support for external CI services. !11176
- Translate backend for Project & Repository pages. !11183
- Fix LaTeX formatting for AsciiDoc wiki. !11212
- Add foreign key for pipeline schedule owner. !11233
- Print Go version in rake gitlab:env:info. !11241
- Include the blob content when printing a blob page. !11247
- Sync email address from specified omniauth provider. !11268 (Robin Bobbitt)
- Disable reference prefixes in notes for Snippets. !11278
- Rename build_events to job_events. !11287
- Add API support for pipeline schedule. !11307 (dosuken123)
- Use route.cache_key for project list cache key. !11325
- Make environment table realtime. !11333
- Cache npm modules between pipelines with yarn to speed up setup-test-env. !11343
- Allow GitLab instance to start when InfluxDB hostname cannot be resolved. !11356
- Add ConvDev Index page to admin area. !11377
- Fix Git-over-HTTP error statuses and improve error messages. !11398
- Renamed users 'Audit Log'' to 'Authentication Log'. !11400
- Style people in issuable search bar. !11402
- Change /builds in the URL to /-/jobs. Backward URLs were also added. !11407
- Update password field label while editing service settings. !11431
- Add an optional performance bar to view performance metrics for the current page. !11439
- Update task_list to version 2.0.0. !11525 (Jared Deckard <jared.deckard@gmail.com>)
- Avoid resource intensive login checks if password is not provided. !11537 (Horatiu Eugen Vlad)
- Allow numeric pages domain. !11550
- Exclude manual actions when checking if pipeline can be canceled. !11562
- Add server uptime to System Info page in admin dashboard. !11590 (Justin Boltz)
- Simplify testing and saving service integrations. !11599
- Fixed handling of the `can_push` attribute in the v3 deploy_keys api. !11607 (Richard Clamp)
- Improve user experience around slash commands in instant comments. !11612
- Show current user immediately in issuable filters. !11630
- Add extra context-sensitive functionality for the top right menu button. !11632
- Reorder Issue action buttons in order of usability. !11642
- Expose atom links with an RSS token instead of using the private token. !11647 (Alexis Reigel)
- Respect merge, instead of push, permissions for protected actions. !11648
- Job details page update real time. !11651
- Improve performance of ProjectFinder used in /projects API endpoint. !11666
- Remove redundant data-turbolink attributes from links. !11672 (blackst0ne)
- Minimum postgresql version is now 9.2. !11677
- Add protected variables which would only be passed to protected branches or protected tags. !11688
- Introduce optimistic locking support via optional parameter last_commit_sha on File Update API. !11694 (electroma)
- Add $CI_ENVIRONMENT_URL to predefined variables for pipelines. !11695
- Simplify project repository settings page. !11698
- Fix pipeline_schedules pages throwing error 500. !11706 (dosuken123)
- Add performance deltas between app deployments on Merge Request widget. !11730
- Add feature toggles and API endpoints for admins. !11747
- Replace 'starred_projects.feature' spinach test with an rspec analog. !11752 (blackst0ne)
- Introduce an Events API. !11755
- Display Shared Runner status in Admin Dashboard. !11783 (Ivan Chernov)
- Persist pipeline stages in the database. !11790
- Revert the feature that would include the current user's username in the HTTP clone URL. !11792
- Enable Gitaly by default in installations from source. !11796
- Use zopfli compression for frontend assets. !11798
- Add tag_list param to project api. !11799 (Ivan Chernov)
- Add changelog for improved Registry description. !11816
- Automatically adjust project settings to match changes in project visibility. !11831
- Add slugify project path to CI enviroment variables. !11838 (Ivan Chernov)
- Add all pipeline sources as special keywords to 'only' and 'except'. !11844 (Filip Krakowski)
- Allow pulling of container images using personal access tokens. !11845
- Expose import_status in Projects API. !11851 (Robin Bobbitt)
- Allow admins to delete users from the admin users page. !11852
- Allow users to be hard-deleted from the API. !11853
- Fix hard-deleting users when they have authored issues. !11855
- Fix missing optional path parameter in "Create project for user" API. !11868
- Allow users to be hard-deleted from the admin panel. !11874
- Add a Rake task to aid in rotating otp_key_base. !11881
- Fix submodule link to then project under subgroup. !11906
- Fix binary encoding error on MR diffs. !11929
- Limit non-administrators to adding 100 members at a time to groups and projects. !11940
- add bulgarian translation of cycle analytics page to I18N. !11958 (Lyubomir Vasilev)
- Make backup task to continue on corrupt repositories. !11962
- Fix incorrect ETag cache key when relative instance URL is used. !11964
- Reinstate is_admin flag in users api when authenticated user is an admin. !12211 (rickettm)
- Fix edit button for deploy keys available from other projects. !12301 (Alexander Randa)
- Fix passing CI_ENVIRONMENT_NAME and CI_ENVIRONMENT_SLUG for CI_ENVIRONMENT_URL. !12344
- Disable environment list refresh due to bug https://gitlab.com/gitlab-org/gitlab-ee/issues/2677. !12347
- Standardize timeline note margins across different viewport sizes. !12364
- Fix Ordered Task List Items. !31483 (Jared Deckard <jared.deckard@gmail.com>)
- Upgrade dependency to Go 1.8.3. !31943
- Add prometheus metrics on pipeline creation.
- Fix etag route not being a match for environments.
- Sort folder for environments.
- Support descriptions for snippets.
- Hide clone panel and file list when user is only a guest. (James Clark)
- Dont create comment on JIRA if it already exists for the entity.
- Update Dashboard Groups UI with better support for subgroups.
- Confirm Project forking behaviour via the API.
- Add prometheus based metrics collection to gitlab webapp.
- Fix: Wiki is not searchable with Guest permissions.
- Center all empty states.
- Remove 'New issue' button when issues search returns no results.
- Add API URL to JIRA settings.
- animate adding issue to boards.
- Update session cookie key name to be unique to instance in development.
- Single click on filter to open filtered search dropdown.
- Makes header information of pipeline show page realtine.
- Creates a mediator for pipeline details vue in order to mount several vue apps with the same data.
- Scope issue/merge request recent searches to project.
- Increase individual diff collapse limit to 100 KB, and render limit to 200 KB.
- Fix Pipelines table empty state - only render empty state if we receive 0 pipelines.
- Make New environment empty state btn lowercase.
- Removes duplicate environment variable in documentation.
- Change links in issuable meta to black.
- Fix border-bottom for project activity tab.
- Adds new icon for CI skipped status.
- Create equal padding for emoji.
- Use briefcase icon for company in profile page.
- Remove overflow from comment form for confidential issues and vertically aligns confidential issue icon.
- Keep trailing newline when resolving conflicts by picking sides.
- Fix /unsubscribe slash command creating extra todos when you were already mentioned in an issue.
- Fix math rendering on blob pages.
- Allow group reporters to manage group labels.
- Use pre-wrap for commit messages to keep lists indented.
- Count badges depend on translucent color to better adjust to different background colors and permission badges now feature a pill shaped design similar to labels.
- Allow reporters to promote project labels to group labels.
- Enabled keyboard shortcuts on artifacts pages.
- Perform filtered search when state tab is changed.
- Remove duplication for sharing projects with groups in project settings.
- Change order of commits ahead and behind on divergence graph for branch list view.
- Creates CI Header component for Pipelines and Jobs details pages.
- Invalidate cache for issue and MR counters more granularly.
- disable blocked manual actions.
- Load tree readme asynchronously.
- Display extra info about files on .gitlab-ci.yml, .gitlab/route-map.yml and LICENSE blob pages.
- Fix replying to a commit discussion displayed in the context of an MR.
- Consistently use monospace font for commit SHAs and branch and tag names.
- Consistently display last push event widget.
- Don't copy empty elements that were not selected on purpose as GFM.
- Copy as GFM even when parts of other elements are selected.
- Autolink package names in Gemfile.
- Resolve N+1 query issue with discussions.
- Don't match email addresses or foo@bar as user references.
- Fix title of discussion jump button at top of page.
- Don't return nil for missing objects from parser cache.
- Make .gitmodules parsing more resilient to syntax errors.
- Add username parameter to gravatar URL.
- Autolink package names in more dependency files.
- Return nil when looking up config for unknown LDAP provider.
- Add system note with link to diff comparison when MR discussion becomes outdated.
- Don't wrap pasted code when it's already inside code tags.
- Revert 'New file from interface on existing branch'.
- Show last commit for current tree on tree page.
- Add documentation about adding foreign keys.
- add username field to push webhook. (David Turner)
- Rename CI/CD Pipelines to Pipelines in the project settings.
- Make environment tables responsive.
- Expand/collapse backlog & closed lists in issue boards.
- Fix GitHub importer performance on branch existence check.
- Fix counter cache for acts as taggable.
- Github - Fix token interpolation when cloning wiki repository.
- Fix token interpolation when setting the Github remote.
- Fix N+1 queries for non-members in comment threads.
- Fix terminals support for Kubernetes Service.
- Fix: A diff comment on a change at last line of a file shows as two comments in discussion.
- Instrument MergeRequestDiff#load_commits.
- Introduce source to Pipeline entity.
- Fixed create new label form in issue form not working for sub-group projects.
- Fixed style on unsubscribe page. (Gustav Ernberg)
- Enables inline editing for an issues title & description.
- Ask for an example project for bug reports.
- Add summary lines for collapsed details in the bug report template.
- Prevent commits from upstream repositories to be re-processed by forks.
- Avoid repeated queries for pipeline builds on merge requests.
- Preloads head pipeline for merge request collection.
- Handle head pipeline when creating merge requests.
- Migrate artifacts to a new path.
- Rescue OpenSSL::SSL::SSLError in JiraService & IssueTrackerService.
- Repository browser: handle in-repository submodule urls. (David Turner)
- Prevent project transfers if a new group is not selected.
- Allow 'no one' as an option for allowed to merge on a procted branch.
- Reduce time spent waiting for certain Sidekiq jobs to complete.
- Refactor ProjectsFinder#init_collection to produce more efficient queries for retrieving projects.
- Remove unused code and uses underscore.
- Restricts search projects dropdown to group projects when group is selected.
- Properly handle container registry redirects to fix metadata stored on a S3 backend.
- Fix LFS timeouts when trying to save large files.
- Set artifact working directory to be in the destination store to prevent unnecessary I/O.
- Strip trailing whitespaces in submodule URLs.
- Make sure reCAPTCHA configuration is loaded when spam checks are initiated.
- Fix up arrow not editing last discussion comment.
- Added application readiness endpoints to the monitoring health check admin view.
- Use wait_for_requests for both ajax and Vue requests.
- Cleanup ci_variables schema and table.
- Remove foreigh key on ci_trigger_schedules only if it exists.
- Allow translation of Pipeline Schedules.
## 9.2.7 (2017-06-21)
- Reinstate is_admin flag in users api when authenticated user is an admin. !12211 (rickettm)

View file

@ -49,6 +49,8 @@ _This notice should stay as the first item in the CONTRIBUTING.MD file._
Thank you for your interest in contributing to GitLab. This guide details how
to contribute to GitLab in a way that is efficient for everyone.
Looking for something to work on? Look for the label [Accepting Merge Requests](#i-want-to-contribute).
GitLab comes into two flavors, GitLab Community Edition (CE) our free and open
source edition, and GitLab Enterprise Edition (EE) which is our commercial
edition. Throughout this guide you will see references to CE and EE for

View file

@ -1 +1 @@
5.0.5
5.0.6

View file

@ -2,6 +2,7 @@ source 'https://rubygems.org'
gem 'rails', '4.2.8'
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
gem 'bootsnap', '~> 1.1'
# Responders respond_to and respond_with
gem 'responders', '~> 2.0'
@ -256,7 +257,7 @@ gem 'base32', '~> 0.3.0'
# Sentry integration
gem 'sentry-raven', '~> 2.4.0'
gem 'premailer-rails', '~> 1.9.0'
gem 'premailer-rails', '~> 1.9.7'
# I18n
gem 'ruby_parser', '~> 3.8', require: false
@ -354,7 +355,7 @@ group :test do
gem 'shoulda-matchers', '~> 2.8.0', require: false
gem 'email_spec', '~> 1.6.0'
gem 'json-schema', '~> 2.6.2'
gem 'webmock', '~> 1.24.0'
gem 'webmock', '~> 2.3.2'
gem 'test_after_commit', '~> 1.1'
gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0'

View file

@ -83,6 +83,8 @@ GEM
bindata (2.3.5)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootsnap (1.1.1)
msgpack (~> 1.0)
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
@ -137,7 +139,7 @@ GEM
crack (0.4.3)
safe_yaml (~> 1.0.0)
creole (0.5.0)
css_parser (1.4.1)
css_parser (1.5.0)
addressable
d3_rails (3.5.11)
railties (>= 3.1.0)
@ -352,7 +354,7 @@ GEM
grape-entity (0.6.0)
activesupport
multi_json (>= 1.3.2)
grpc (1.2.5)
grpc (1.4.0)
google-protobuf (~> 3.1)
googleauth (~> 0.5.1)
haml (4.0.7)
@ -366,7 +368,7 @@ GEM
temple (~> 0.7.6)
thor
tilt
hashdiff (0.3.2)
hashdiff (0.3.4)
hashie (3.5.5)
hashie-forbidden_attributes (0.1.1)
hashie (>= 3.0)
@ -461,8 +463,9 @@ GEM
mimemagic (0.3.0)
mini_portile2 (2.1.0)
minitest (5.7.0)
mmap2 (2.2.6)
mmap2 (2.2.7)
mousetrap-rails (1.4.6)
msgpack (1.1.0)
multi_json (1.12.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
@ -589,10 +592,11 @@ GEM
websocket-driver (>= 0.2.0)
posix-spawn (0.3.11)
powerpack (0.1.1)
premailer (1.8.6)
css_parser (>= 1.3.6)
premailer (1.10.4)
addressable
css_parser (>= 1.4.10)
htmlentities (>= 4.0.0)
premailer-rails (1.9.2)
premailer-rails (1.9.7)
actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9)
prometheus-client-mmap (0.7.0.beta5)
@ -887,7 +891,7 @@ GEM
vmstat (2.3.0)
warden (1.2.6)
rack (>= 1.0)
webmock (1.24.6)
webmock (2.3.2)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
@ -926,6 +930,7 @@ DEPENDENCIES
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2)
bootsnap (~> 1.1)
bootstrap-sass (~> 3.3.0)
bootstrap_form (~> 2.7.0)
brakeman (~> 3.6.0)
@ -1045,7 +1050,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.0)
premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta5)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
@ -1117,7 +1122,7 @@ DEPENDENCIES
version_sorter (~> 2.1.0)
virtus (~> 1.0.1)
vmstat (~> 2.3.0)
webmock (~> 1.24.0)
webmock (~> 2.3.2)
webpack-rails (~> 0.9.10)
wikicloth (= 0.8.1)

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -2,11 +2,7 @@
/* global Flash */
import Cookies from 'js-cookie';
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
import { glEmojiTag } from './behaviors/gl_emoji';
import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid';
import * as Emoji from './emoji';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
@ -17,8 +13,6 @@ const requestAnimationFrame = window.requestAnimationFrame ||
const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence
let categoryMap = null;
const categoryLabelMap = {
activity: 'Activity',
people: 'People',
@ -30,26 +24,6 @@ const categoryLabelMap = {
flags: 'Flags',
};
function buildCategoryMap() {
return Object.keys(emojiMap).reduce((currentCategoryMap, emojiNameKey) => {
const emojiInfo = emojiMap[emojiNameKey];
if (currentCategoryMap[emojiInfo.category]) {
currentCategoryMap[emojiInfo.category].push(emojiNameKey);
}
return currentCategoryMap;
}, {
activity: [],
people: [],
nature: [],
food: [],
travel: [],
objects: [],
symbols: [],
flags: [],
});
}
function renderCategory(name, emojiList, opts = {}) {
return `
<h5 class="emoji-menu-title">
@ -59,7 +33,7 @@ function renderCategory(name, emojiList, opts = {}) {
${emojiList.map(emojiName => `
<li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button">
${glEmojiTag(emojiName, {
${Emoji.glEmojiTag(emojiName, {
sprite: true,
})}
</button>
@ -72,7 +46,6 @@ function renderCategory(name, emojiList, opts = {}) {
export default class AwardsHandler {
constructor() {
this.eventListeners = [];
this.aliases = emojiAliases;
// If the user shows intent let's pre-build the menu
this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => {
const $menu = $('.emoji-menu');
@ -81,8 +54,6 @@ export default class AwardsHandler {
this.createEmojiMenu();
});
}
// Prebuild the categoryMap
categoryMap = categoryMap || buildCategoryMap();
});
this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => {
e.stopPropagation();
@ -168,7 +139,7 @@ export default class AwardsHandler {
this.isCreatingEmojiMenu = true;
// Render the first category
categoryMap = categoryMap || buildCategoryMap();
const categoryMap = Emoji.getEmojiCategoryMap();
const categoryNameKey = Object.keys(categoryMap)[0];
const emojisInCategory = categoryMap[categoryNameKey];
const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
@ -208,7 +179,7 @@ export default class AwardsHandler {
}
this.isAddingRemainingEmojiMenuCategories = true;
categoryMap = categoryMap || buildCategoryMap();
const categoryMap = Emoji.getEmojiCategoryMap();
// Avoid the jank and render the remaining categories separately
// This will take more time, but makes UI more responsive
@ -262,14 +233,8 @@ export default class AwardsHandler {
return $menu.css(css);
}
addAward(
votesBlock,
awardUrl,
emoji,
checkMutuality,
callback,
) {
const normalizedEmoji = this.normalizeEmojiName(emoji);
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
const normalizedEmoji = Emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
@ -279,16 +244,12 @@ export default class AwardsHandler {
$('.js-add-award.is-active').removeClass('is-active');
}
addAwardToEmojiBar(
votesBlock,
emoji,
checkForMutuality,
) {
addAwardToEmojiBar(votesBlock, emoji, checkForMutuality) {
if (checkForMutuality || checkForMutuality === null) {
this.checkMutuality(votesBlock, emoji);
}
this.addEmojiToFrequentlyUsedList(emoji);
const normalizedEmoji = this.normalizeEmojiName(emoji);
const normalizedEmoji = Emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
if ($emojiButton.length > 0) {
if (this.isActive($emojiButton)) {
@ -413,7 +374,7 @@ export default class AwardsHandler {
createAwardButtonForVotesBlock(votesBlock, emojiName) {
const buttonHtml = `
<button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom">
${glEmojiTag(emojiName)}
${Emoji.glEmojiTag(emojiName)}
<span class="award-control-text js-counter">1</span>
</button>
`;
@ -478,12 +439,8 @@ export default class AwardsHandler {
return $('body, html').animate(options, 200);
}
normalizeEmojiName(emoji) {
return Object.prototype.hasOwnProperty.call(this.aliases, emoji) ? this.aliases[emoji] : emoji;
}
addEmojiToFrequentlyUsedList(emoji) {
if (isEmojiNameValid(emoji)) {
if (Emoji.isEmojiNameValid(emoji)) {
this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji));
Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 });
}
@ -493,7 +450,7 @@ export default class AwardsHandler {
return this.frequentlyUsedEmojis || (() => {
const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
inputName => isEmojiNameValid(inputName),
inputName => Emoji.isEmojiNameValid(inputName),
);
return this.frequentlyUsedEmojis;
@ -535,21 +492,11 @@ export default class AwardsHandler {
}
}
findMatchingEmojiElements(term) {
const safeTerm = term.toLowerCase();
const namesMatchingAlias = [];
Object.keys(emojiAliases).forEach((alias) => {
if (alias.indexOf(safeTerm) >= 0) {
namesMatchingAlias.push(emojiAliases[alias]);
}
});
const $matchingElements = namesMatchingAlias.concat(safeTerm)
.reduce(
($result, searchTerm) =>
$result.add($(`.emoji-menu-list:not(.frequent-emojis) [data-name*="${searchTerm}"]`)),
$([]),
);
findMatchingEmojiElements(query) {
const emojiMatches = Emoji.filterEmojiNamesByAlias(query);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
const $matchingElements = $emojiElements
.filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0);
return $matchingElements.closest('li').clone();
}

View file

@ -1,75 +1,10 @@
import installCustomElements from 'document-register-element';
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
import { getUnicodeSupportMap } from './gl_emoji/unicode_support_map';
import { isEmojiUnicodeSupported } from './gl_emoji/is_emoji_unicode_supported';
import { emojiImageTag, emojiFallbackImageSrc } from '../emoji';
import isEmojiUnicodeSupported from '../emoji/support';
installCustomElements(window);
const generatedUnicodeSupportMap = getUnicodeSupportMap();
function emojiImageTag(name, src) {
return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`;
}
function assembleFallbackImageSrc(inputName) {
let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName;
let emojiInfo = emojiMap[name];
// Fallback to question mark for unknown emojis
if (!emojiInfo) {
name = 'grey_question';
emojiInfo = emojiMap[name];
}
const fallbackImageSrc = `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${emojiInfo.digest}.png`;
return fallbackImageSrc;
}
const glEmojiTagDefaults = {
sprite: false,
forceFallback: false,
};
function glEmojiTag(inputName, options) {
const opts = Object.assign({}, glEmojiTagDefaults, options);
let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName;
let emojiInfo = emojiMap[name];
// Fallback to question mark for unknown emojis
if (!emojiInfo) {
name = 'grey_question';
emojiInfo = emojiMap[name];
}
const fallbackImageSrc = assembleFallbackImageSrc(name);
const fallbackSpriteClass = `emoji-${name}`;
const classList = [];
if (opts.forceFallback && opts.sprite) {
classList.push('emoji-icon');
classList.push(fallbackSpriteClass);
}
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc);
}
return `
<gl-emoji
${classAttribute}
data-name="${name}"
data-fallback-src="${fallbackImageSrc}"
${fallbackSpriteAttribute}
data-unicode-version="${emojiInfo.unicodeVersion}"
title="${emojiInfo.description}"
>
${contents}
</gl-emoji>
`;
}
function installGlEmojiElement() {
export default function installGlEmojiElement() {
const GlEmojiElementProto = Object.create(HTMLElement.prototype);
GlEmojiElementProto.createdCallback = function createdCallback() {
const emojiUnicode = this.textContent.trim();
@ -90,7 +25,7 @@ function installGlEmojiElement() {
if (
emojiUnicode &&
isEmojiUnicode &&
!isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion)
!isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
) {
// CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) {
@ -100,7 +35,7 @@ function installGlEmojiElement() {
} else if (hasImageFallback) {
this.innerHTML = emojiImageTag(name, fallbackSrc);
} else {
const src = assembleFallbackImageSrc(name);
const src = emojiFallbackImageSrc(name);
this.innerHTML = emojiImageTag(name, src);
}
}
@ -110,9 +45,3 @@ function installGlEmojiElement() {
prototype: GlEmojiElementProto,
});
}
export {
installGlEmojiElement,
glEmojiTag,
emojiImageTag,
};

View file

@ -1,11 +0,0 @@
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
function isEmojiNameValid(inputName) {
const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName;
return name && emojiMap[name];
}
export default isEmojiNameValid;

View file

@ -1,7 +1,7 @@
import './autosize';
import './bind_in_out';
import './details_behavior';
import { installGlEmojiElement } from './gl_emoji';
import installGlEmojiElement from './gl_emoji';
import './quick_submit';
import './requires_input';
import './toggler_behavior';

View file

@ -40,7 +40,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
e.preventDefault();
const $form = $(e.target).closest('form');
const $submitButton = $form.find('input[type=submit], button[type=submit]');
const $submitButton = $form.find('input[type=submit], button[type=submit]').first();
if (!$submitButton.attr('disabled')) {
$submitButton.trigger('click', [e]);

View file

@ -85,9 +85,8 @@ window.Build = (function () {
if (!this.hasBeenScrolled) {
this.scrollToBottom();
}
});
this.verifyTopPosition();
})
.then(() => this.verifyTopPosition());
}
Build.prototype.canScroll = function () {
@ -176,7 +175,7 @@ window.Build = (function () {
}
if ($flashError.length) {
topPostion += $flashError.outerHeight();
topPostion += $flashError.outerHeight() + prependTopDefault;
}
this.$buildTrace.css({
@ -196,6 +195,7 @@ window.Build = (function () {
})
.done((log) => {
gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
if (log.state) {
this.state = log.state;
}
@ -220,7 +220,11 @@ window.Build = (function () {
}
if (!log.complete) {
this.toggleScrollAnimation(true);
if (!this.hasBeenScrolled) {
this.toggleScrollAnimation(true);
} else {
this.toggleScrollAnimation(false);
}
Build.timeout = setTimeout(() => {
//eslint-disable-next-line
@ -229,7 +233,8 @@ window.Build = (function () {
if (!this.hasBeenScrolled) {
this.scrollToBottom();
}
});
})
.then(() => this.verifyTopPosition());
}, 4000);
} else {
this.$buildRefreshAnimation.remove();

View file

@ -1,6 +1,7 @@
/* eslint-disable class-methods-use-this */
import './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
const UNFOLD_COUNT = 20;
let isBound = false;
@ -8,8 +9,10 @@ let isBound = false;
class Diff {
constructor() {
const $diffFile = $('.files .diff-file');
$diffFile.singleFileDiff();
$diffFile.filesCommentButton();
FilesCommentButton.init($diffFile);
$diffFile.each((index, file) => new gl.ImageFile(file));

View file

@ -139,9 +139,9 @@ const DiffNoteAvatars = Vue.extend({
const notesCount = this.notesCount;
$(this.$el).closest('.js-avatar-container')
.toggleClass('js-no-comment-btn', notesCount > 0)
.toggleClass('no-comment-btn', notesCount > 0)
.nextUntil('.js-avatar-container')
.toggleClass('js-no-comment-btn', notesCount > 0);
.toggleClass('no-comment-btn', notesCount > 0);
},
toggleDiscussionsToggleState() {
const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');

View file

@ -55,6 +55,7 @@ import RefSelectDropdown from './ref_select_dropdown';
import GfmAutoComplete from './gfm_auto_complete';
import ShortcutsBlob from './shortcuts_blob';
import initSettingsPanels from './settings_panels';
import initExperimentalFlags from './experimental_flags';
(function() {
var Dispatcher;
@ -120,6 +121,9 @@ import initSettingsPanels from './settings_panels';
}
switch (page) {
case 'profiles:preferences:show':
initExperimentalFlags();
break;
case 'sessions:new':
new UsernameValidator();
new ActiveTabMemoizer();
@ -205,8 +209,8 @@ import initSettingsPanels from './settings_panels';
new MilestoneSelect();
new gl.IssuableTemplateSelectors();
break;
case 'projects:merge_requests:new':
case 'projects:merge_requests:new_diffs':
case 'projects:merge_requests:creations:new':
case 'projects:merge_requests:creations:diffs':
case 'projects:merge_requests:edit':
new gl.Diff();
shortcut_handler = new ShortcutsNavigation();
@ -243,10 +247,6 @@ import initSettingsPanels from './settings_panels';
shortcut_handler = new ShortcutsIssuable(true);
new ZenMode();
break;
case "projects:merge_requests:diffs":
new gl.Diff();
new ZenMode();
break;
case 'dashboard:activity':
new gl.Activities();
break;
@ -315,7 +315,7 @@ import initSettingsPanels from './settings_panels';
new gl.Members();
new UsersSelect();
break;
case 'projects:members:show':
case 'projects:settings:members:show':
new gl.MemberExpirationDate('.js-access-expiration-date-groups');
new GroupsSelect();
new gl.MemberExpirationDate();
@ -382,7 +382,7 @@ import initSettingsPanels from './settings_panels';
case 'search:show':
new Search();
break;
case 'projects:repository:show':
case 'projects:settings:repository:show':
// Initialize Protected Branch Settings
new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList();
@ -392,7 +392,7 @@ import initSettingsPanels from './settings_panels';
// Initialize expandable settings panels
initSettingsPanels();
break;
case 'projects:ci_cd:show':
case 'projects:settings:ci_cd:show':
new gl.ProjectVariables();
break;
case 'ci:lints:create':

View file

@ -0,0 +1,99 @@
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
export const validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
export function normalizeEmojiName(name) {
return Object.prototype.hasOwnProperty.call(emojiAliases, name) ? emojiAliases[name] : name;
}
export function isEmojiNameValid(name) {
return validEmojiNames.indexOf(name) >= 0;
}
export function filterEmojiNames(filter) {
const match = filter.toLowerCase();
return validEmojiNames.filter(name => name.indexOf(match) >= 0);
}
export function filterEmojiNamesByAlias(filter) {
return _.uniq(filterEmojiNames(filter).map(name => normalizeEmojiName(name)));
}
let emojiCategoryMap;
export function getEmojiCategoryMap() {
if (!emojiCategoryMap) {
emojiCategoryMap = {
activity: [],
people: [],
nature: [],
food: [],
travel: [],
objects: [],
symbols: [],
flags: [],
};
Object.keys(emojiMap).forEach((name) => {
const emoji = emojiMap[name];
if (emojiCategoryMap[emoji.category]) {
emojiCategoryMap[emoji.category].push(name);
}
});
}
return emojiCategoryMap;
}
export function getEmojiInfo(query) {
let name = normalizeEmojiName(query);
let emojiInfo = emojiMap[name];
// Fallback to question mark for unknown emojis
if (!emojiInfo) {
name = 'grey_question';
emojiInfo = emojiMap[name];
}
return { ...emojiInfo, name };
}
export function emojiFallbackImageSrc(inputName) {
const { name, digest } = getEmojiInfo(inputName);
return `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${digest}.png`;
}
export function emojiImageTag(name, src) {
return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`;
}
export function glEmojiTag(inputName, options) {
const opts = { sprite: false, forceFallback: false, ...options };
const { name, ...emojiInfo } = getEmojiInfo(inputName);
const fallbackImageSrc = emojiFallbackImageSrc(name);
const fallbackSpriteClass = `emoji-${name}`;
const classList = [];
if (opts.forceFallback && opts.sprite) {
classList.push('emoji-icon');
classList.push(fallbackSpriteClass);
}
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc);
}
return `
<gl-emoji
${classAttribute}
data-name="${name}"
data-fallback-src="${fallbackImageSrc}"
${fallbackSpriteAttribute}
data-unicode-version="${emojiInfo.unicodeVersion}"
title="${emojiInfo.description}"
>
${contents}
</gl-emoji>
`;
}

View file

@ -0,0 +1,10 @@
import isEmojiUnicodeSupported from './is_emoji_unicode_supported';
import getUnicodeSupportMap from './unicode_support_map';
// cache browser support map between calls
let browserUnicodeSupportMap;
export default function isEmojiUnicodeSupportedByBrowser(emojiUnicode, unicodeVersion) {
browserUnicodeSupportMap = browserUnicodeSupportMap || getUnicodeSupportMap();
return isEmojiUnicodeSupported(browserUnicodeSupportMap, emojiUnicode, unicodeVersion);
}

View file

@ -111,7 +111,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe
}
export {
isEmojiUnicodeSupported,
isEmojiUnicodeSupported as default,
isFlagEmoji,
isKeycapEmoji,
isSkinToneComboEmoji,

View file

@ -140,7 +140,7 @@ function generateUnicodeSupportMap(testMap) {
return resultMap;
}
function getUnicodeSupportMap() {
export default function getUnicodeSupportMap() {
let unicodeSupportMap;
let userAgentFromCache;
@ -165,8 +165,3 @@ function getUnicodeSupportMap() {
return unicodeSupportMap;
}
export {
getUnicodeSupportMap,
generateUnicodeSupportMap,
};

View file

@ -2,6 +2,7 @@
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
@ -12,6 +13,10 @@ export default {
},
},
directives: {
tooltip,
},
components: {
loadingIcon,
},
@ -33,8 +38,6 @@ export default {
onClickAction(endpoint) {
this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
eventHub.$emit('postAction', endpoint);
},
@ -53,11 +56,11 @@ export default {
class="btn-group"
role="group">
<button
v-tooltip
type="button"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
data-container="body"
data-toggle="dropdown"
ref="tooltip"
:title="title"
:aria-label="title"
:disabled="isLoading">

View file

@ -1,4 +1,6 @@
<script>
import tooltip from '../../vue_shared/directives/tooltip';
/**
* Renders the external url link in environments table.
*/
@ -10,6 +12,10 @@ export default {
},
},
directives: {
tooltip,
},
computed: {
title() {
return 'Open';
@ -19,7 +25,8 @@ export default {
</script>
<template>
<a
class="btn external-url has-tooltip"
v-tooltip
class="btn external-url"
data-container="body"
target="_blank"
rel="noopener noreferrer nofollow"

View file

@ -2,6 +2,8 @@
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
monitoringUrl: {
@ -10,6 +12,10 @@ export default {
},
},
directives: {
tooltip,
},
computed: {
title() {
return 'Monitoring';
@ -19,7 +25,8 @@ export default {
</script>
<template>
<a
class="btn monitoring-url has-tooltip hidden-xs hidden-sm"
v-tooltip
class="btn monitoring-url hidden-xs hidden-sm"
data-container="body"
rel="noopener noreferrer nofollow"
:href="monitoringUrl"

View file

@ -5,6 +5,7 @@
*/
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
@ -14,6 +15,10 @@ export default {
},
},
directives: {
tooltip,
},
data() {
return {
isLoading: false,
@ -46,8 +51,9 @@ export default {
</script>
<template>
<button
v-tooltip
type="button"
class="btn stop-env-link has-tooltip hidden-xs hidden-sm"
class="btn stop-env-link hidden-xs hidden-sm"
data-container="body"
@click="onClick"
:disabled="isLoading"

View file

@ -4,6 +4,7 @@
* Used in environments table.
*/
import terminalIconSvg from 'icons/_icon_terminal.svg';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
@ -14,6 +15,10 @@ export default {
},
},
directives: {
tooltip,
},
data() {
return {
terminalIconSvg,
@ -29,7 +34,8 @@ export default {
</script>
<template>
<a
class="btn terminal-button has-tooltip hidden-xs hidden-sm"
v-tooltip
class="btn terminal-button hidden-xs hidden-sm"
data-container="body"
:title="title"
:aria-label="title"

View file

@ -0,0 +1,11 @@
import Cookies from 'js-cookie';
export default () => {
$('.js-experiment-feature-toggle').on('change', (e) => {
const el = e.target;
Cookies.set(el.name, el.value, {
expires: 365 * 10,
});
});
};

View file

@ -1,150 +1,73 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */
/* global FilesCommentButton */
/* global notes */
let $commentButtonTemplate;
/* Developer beware! Do not add logic to showButton or hideButton
* that will force a reflow. Doing so will create a signficant performance
* bottleneck for pages with large diffs. For a comprehensive list of what
* causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a
*/
window.FilesCommentButton = (function() {
var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS;
const LINE_NUMBER_CLASS = 'diff-line-num';
const UNFOLDABLE_LINE_CLASS = 'js-unfold';
const NO_COMMENT_CLASS = 'no-comment-btn';
const EMPTY_CELL_CLASS = 'empty-cell';
const OLD_LINE_CLASS = 'old_line';
const LINE_COLUMN_CLASSES = `.${LINE_NUMBER_CLASS}, .line_content`;
const DIFF_CONTAINER_SELECTOR = '.files';
const DIFF_EXPANDED_CLASS = 'diff-expanded';
COMMENT_BUTTON_CLASS = '.add-diff-note';
export default {
init($diffFile) {
/* Caching is used only when the following members are *true*. This is because there are likely to be
* differently configured versions of diffs in the same session. However if these values are true, they
* will be true in all cases */
LINE_HOLDER_CLASS = '.line_holder';
LINE_NUMBER_CLASS = 'diff-line-num';
LINE_CONTENT_CLASS = 'line_content';
UNFOLDABLE_LINE_CLASS = 'js-unfold';
EMPTY_CELL_CLASS = 'empty-cell';
OLD_LINE_CLASS = 'old_line';
LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content";
TEXT_FILE_SELECTOR = '.text-file';
function FilesCommentButton(filesContainerElement) {
this.render = this.render.bind(this);
this.hideButton = this.hideButton.bind(this);
this.isParallelView = notes.isParallelView();
filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render)
.on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton);
}
FilesCommentButton.prototype.render = function(e) {
var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button;
$currentTarget = $(e.currentTarget);
if ($currentTarget.hasClass('js-no-comment-btn')) return;
lineContentElement = this.getLineContent($currentTarget);
buttonParentElement = this.getButtonParent($currentTarget);
if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return;
$button = $(COMMENT_BUTTON_CLASS, buttonParentElement);
buttonParentElement.addClass('is-over')
.nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over');
if ($button.length) {
return;
if (!this.userCanCreateNote) {
// data-can-create-note is an empty string when true, otherwise undefined
this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === '';
}
textFileElement = this.getTextFileElement($currentTarget);
buttonParentElement.append(this.buildButton({
discussionID: lineContentElement.attr('data-discussion-id'),
lineType: lineContentElement.attr('data-line-type'),
noteableType: textFileElement.attr('data-noteable-type'),
noteableID: textFileElement.attr('data-noteable-id'),
commitID: textFileElement.attr('data-commit-id'),
noteType: lineContentElement.attr('data-note-type'),
// LegacyDiffNote
lineCode: lineContentElement.attr('data-line-code'),
// DiffNote
position: lineContentElement.attr('data-position')
}));
};
FilesCommentButton.prototype.hideButton = function(e) {
var $currentTarget = $(e.currentTarget);
var buttonParentElement = this.getButtonParent($currentTarget);
buttonParentElement.removeClass('is-over')
.nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over');
};
FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
return $commentButtonTemplate.clone().attr({
'data-discussion-id': buttonAttributes.discussionID,
'data-line-type': buttonAttributes.lineType,
'data-noteable-type': buttonAttributes.noteableType,
'data-noteable-id': buttonAttributes.noteableID,
'data-commit-id': buttonAttributes.commitID,
'data-note-type': buttonAttributes.noteType,
// LegacyDiffNote
'data-line-code': buttonAttributes.lineCode,
// DiffNote
'data-position': buttonAttributes.position
});
};
FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) {
return hoveredElement.closest(TEXT_FILE_SELECTOR);
};
FilesCommentButton.prototype.getLineContent = function(hoveredElement) {
if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) {
return hoveredElement;
if (typeof notes !== 'undefined' && !this.isParallelView) {
this.isParallelView = notes.isParallelView && notes.isParallelView();
}
if (!this.isParallelView) {
return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS);
} else {
return $(hoveredElement).next("." + LINE_CONTENT_CLASS);
}
};
FilesCommentButton.prototype.getButtonParent = function(hoveredElement) {
if (!this.isParallelView) {
if (hoveredElement.hasClass(OLD_LINE_CLASS)) {
return hoveredElement;
if (this.userCanCreateNote) {
$diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e))
.on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e));
}
},
showButton(isParallelView, e) {
const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView);
if (!this.validateButtonParent(buttonParentElement)) return;
buttonParentElement.classList.add('is-over');
buttonParentElement.nextElementSibling.classList.add('is-over');
},
hideButton(isParallelView, e) {
const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView);
buttonParentElement.classList.remove('is-over');
buttonParentElement.nextElementSibling.classList.remove('is-over');
},
getButtonParent(hoveredElement, isParallelView) {
if (isParallelView) {
if (!hoveredElement.classList.contains(LINE_NUMBER_CLASS)) {
return hoveredElement.previousElementSibling;
}
return hoveredElement.parent().find("." + OLD_LINE_CLASS);
} else {
if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) {
return hoveredElement;
}
return $(hoveredElement).prev("." + LINE_NUMBER_CLASS);
} else if (!hoveredElement.classList.contains(OLD_LINE_CLASS)) {
return hoveredElement.parentNode.querySelector(`.${OLD_LINE_CLASS}`);
}
};
return hoveredElement;
},
FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) {
return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS);
};
FilesCommentButton.prototype.validateLineContent = function(lineContentElement) {
return lineContentElement.attr('data-note-type') && lineContentElement.attr('data-note-type') !== '';
};
return FilesCommentButton;
})();
$.fn.filesCommentButton = function() {
$commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>');
if (!(this && (this.parent().data('can-create-note') != null))) {
return;
}
return this.each(function() {
if (!$.data(this, 'filesCommentButton')) {
return $.data(this, 'filesCommentButton', new FilesCommentButton($(this)));
}
});
validateButtonParent(buttonParentElement) {
return !buttonParentElement.classList.contains(EMPTY_CELL_CLASS) &&
!buttonParentElement.classList.contains(UNFOLDABLE_LINE_CLASS) &&
!buttonParentElement.classList.contains(NO_COMMENT_CLASS) &&
!buttonParentElement.parentNode.classList.contains(DIFF_EXPANDED_CLASS);
},
};

View file

@ -2,6 +2,7 @@
import AjaxFilter from '~/droplab/plugins/ajax_filter';
import './filtered_search_dropdown';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
class DropdownUser extends gl.FilteredSearchDropdown {
constructor(droplab, dropdown, input, tokenKeys, filter) {
@ -32,8 +33,7 @@ class DropdownUser extends gl.FilteredSearchDropdown {
}
hideCurrentUser() {
const currenUserItem = this.dropdown.querySelector('.js-current-user');
currenUserItem.classList.add('hidden');
addClassIfElementExists(this.dropdown.querySelector('.js-current-user'), 'hidden');
}
itemClicked(e) {

View file

@ -3,6 +3,7 @@ import RecentSearchesRoot from './recent_searches_root';
import RecentSearchesStore from './stores/recent_searches_store';
import RecentSearchesService from './services/recent_searches_service';
import eventHub from './event_hub';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
class FilteredSearchManager {
constructor(page) {
@ -227,11 +228,7 @@ class FilteredSearchManager {
}
addInputContainerFocus() {
const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
if (inputContainer) {
inputContainer.classList.add('focus');
}
addClassIfElementExists(this.filteredSearchInput.closest('.filtered-search-box'), 'focus');
}
removeInputContainerFocus(e) {

View file

@ -1,8 +1,6 @@
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
import { glEmojiTag } from '~/behaviors/gl_emoji';
import glRegexp from '~/lib/utils/regexp';
import AjaxCache from '~/lib/utils/ajax_cache';
import { validEmojiNames, glEmojiTag } from './emoji';
import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache';
function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
@ -375,7 +373,7 @@ class GfmAutoComplete {
if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases)));
this.loadData($input, at, validEmojiNames);
} else {
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
.then((data) => {
@ -398,6 +396,13 @@ class GfmAutoComplete {
this.cachedData = {};
}
destroy() {
this.input.each((i, input) => {
const $input = $(input);
$input.atwho('destroy');
});
}
static isLoading(data) {
let dataToInspect = data;
if (data && data.length > 0) {

View file

@ -21,6 +21,9 @@ function GLForm(form, enableGFM = false) {
GLForm.prototype.destroy = function() {
// Clean form listeners
this.clearEventListeners();
if (this.autoComplete) {
this.autoComplete.destroy();
}
return this.form.data('gl-form', null);
};
@ -33,7 +36,8 @@ GLForm.prototype.setupForm = function() {
this.form.addClass('gfm-form');
// remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(this.form.find('.js-gfm-input'), {
this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
this.autoComplete.setup(this.form.find('.js-gfm-input'), {
emojis: true,
members: this.enableGFM,
issues: this.enableGFM,

View file

@ -47,7 +47,8 @@
ref="textarea"
slot="textarea"
placeholder="Write a comment or drag your files here..."
@keydown.meta.enter="updateIssuable">
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable">
</textarea>
</markdown-field>
</div>

View file

@ -1,10 +1,10 @@
<script>
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
export default {
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
props: {
formState: {
type: Object,
@ -71,9 +71,9 @@
data-placeholder="Move to a different project" />
</div>
<span
v-tooltip
data-placement="auto top"
title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location."
ref="tooltip">
title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.">
<i
class="fa fa-question-circle"
aria-hidden="true">

View file

@ -26,6 +26,7 @@
placeholder="Issue title"
aria-label="Issue title"
v-model="formState.title"
@keydown.meta.enter="updateIssuable" />
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable" />
</fieldset>
</template>

View file

@ -39,6 +39,17 @@
runnerId() {
return `#${this.job.runner.id}`;
},
renderBlock() {
return this.job.merge_request ||
this.job.duration ||
this.job.finished_data ||
this.job.erased_at ||
this.job.queued ||
this.job.runner ||
this.job.coverage ||
this.job.tags.length ||
this.job.cancel_path;
},
},
};
</script>
@ -63,7 +74,7 @@
Retry
</a>
</div>
<div class="block">
<div :class="{block : renderBlock }">
<p
class="build-detail-row js-job-mr"
v-if="job.merge_request">

View file

@ -21,6 +21,7 @@
}
bindEvents() {
this.prioritizedLabels.find('.btn-action').on('mousedown', this, this.onButtonActionClick);
return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
}
@ -36,6 +37,11 @@
_this.toggleEmptyState($label, $btn, action);
}
onButtonActionClick(e) {
e.stopPropagation();
$(e.currentTarget).tooltip('hide');
}
toggleEmptyState($label, $btn, action) {
this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li'));
}

View file

@ -0,0 +1,7 @@
/* eslint-disable import/prefer-default-export */
export const addClassIfElementExists = (element, className) => {
if (element) {
element.classList.add(className);
}
};

View file

@ -94,8 +94,8 @@ gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) {
if (blockTag != null) {
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
if (blockTag != null && blockTag !== '') {
insertText = this.blockTagText(text, textArea, blockTag, selected);
} else {
insertText = selectedSplit.map(function(val) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -299,9 +299,10 @@ $(function () {
// Commit show suppressed diff
});
$('.navbar-toggle').on('click', function () {
$('.header-content .title').toggle();
$('.header-content .title, .header-content .navbar-sub-nav').toggle();
$('.header-content .header-logo').toggle();
$('.header-content .navbar-collapse').toggle();
$('.js-navbar-toggle-left, .js-navbar-toggle-right, .title-container').toggle();
return $('.navbar-toggle').toggleClass('active');
});
// Show/hide comments on diff

View file

@ -144,7 +144,9 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
this.resetViewContainer();
this.mountPipelinesView();
} else {
this.expandView();
if (Breakpoints.get().getBreakpointSize() !== 'xs') {
this.expandView();
}
this.resetViewContainer();
this.destroyPipelinesView();
}
@ -168,9 +170,8 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
// Activate a tab based on the current action
activateTab(action) {
const activate = action === 'show' ? 'notes' : action;
// important note: the .tab('show') method triggers 'shown.bs.tab' event itself
$(`.merge-request-tabs a[data-action='${activate}']`).tab('show');
$(`.merge-request-tabs a[data-action='${action}']`).tab('show');
}
// Replaces the current Merge Request-specific action in the URL with a new one
@ -185,7 +186,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
// location.pathname # => "/namespace/project/merge_requests/1/diffs"
//
// location.pathname # => "/namespace/project/merge_requests/1/diffs"
// setCurrentAction('notes')
// setCurrentAction('show')
// location.pathname # => "/namespace/project/merge_requests/1"
//
// location.pathname # => "/namespace/project/merge_requests/1/diffs"
@ -194,13 +195,13 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
//
// Returns the new URL String
setCurrentAction(action) {
this.currentAction = action === 'show' ? 'notes' : action;
this.currentAction = action;
// Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs'
let newState = location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
// Remove a trailing '/commits' '/diffs' '/pipelines'
let newState = location.pathname.replace(/\/(commits|diffs|pipelines)(\.html)?\/?$/, '');
// Append the new action if we're on a tab other than 'notes'
if (this.currentAction !== 'notes') {
if (this.currentAction !== 'show' && this.currentAction !== 'new') {
newState += `/${this.currentAction}`;
}

View file

@ -0,0 +1,157 @@
<script>
/* global Flash */
import _ from 'underscore';
import statusCodes from '../../lib/utils/http_status';
import MonitoringService from '../services/monitoring_service';
import monitoringRow from './monitoring_row.vue';
import monitoringState from './monitoring_state.vue';
import MonitoringStore from '../stores/monitoring_store';
import eventHub from '../event_hub';
export default {
data() {
const metricsData = document.querySelector('#prometheus-graphs').dataset;
const store = new MonitoringStore();
return {
store,
state: 'gettingStarted',
hasMetrics: gl.utils.convertPermissionToBoolean(metricsData.hasMetrics),
documentationPath: metricsData.documentationPath,
settingsPath: metricsData.settingsPath,
endpoint: metricsData.additionalMetrics,
deploymentEndpoint: metricsData.deploymentEndpoint,
showEmptyState: true,
backOffRequestCounter: 0,
updateAspectRatio: false,
updatedAspectRatios: 0,
resizeThrottled: {},
};
},
components: {
monitoringRow,
monitoringState,
},
methods: {
getGraphsData() {
const maxNumberOfRequests = 3;
this.state = 'loading';
gl.utils.backOff((next, stop) => {
this.service.get().then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < maxNumberOfRequests) {
next();
} else {
stop(new Error('Failed to connect to the prometheus server'));
}
} else {
stop(resp);
}
}).catch(stop);
})
.then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
this.state = 'unableToConnect';
return false;
}
return resp.json();
})
.then((metricGroupsData) => {
if (!metricGroupsData) return false;
this.store.storeMetrics(metricGroupsData.data);
return this.getDeploymentData();
})
.then((deploymentData) => {
if (deploymentData !== false) {
this.store.storeDeploymentData(deploymentData.deployments);
this.showEmptyState = false;
}
return {};
})
.catch(() => {
this.state = 'unableToConnect';
});
},
getDeploymentData() {
return this.service.getDeploymentData(this.deploymentEndpoint)
.then(resp => resp.json())
.catch(() => new Flash('Error getting deployment information.'));
},
resize() {
this.updateAspectRatio = true;
},
toggleAspectRatio() {
this.updatedAspectRatios = this.updatedAspectRatios += 1;
if (this.store.getMetricsCount() === this.updatedAspectRatios) {
this.updateAspectRatio = !this.updateAspectRatio;
this.updatedAspectRatios = 0;
}
},
},
created() {
this.service = new MonitoringService(this.endpoint);
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
},
beforeDestroy() {
eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
window.removeEventListener('resize', this.resizeThrottled, false);
},
mounted() {
this.resizeThrottled = _.throttle(this.resize, 600);
if (!this.hasMetrics) {
this.state = 'gettingStarted';
} else {
this.getGraphsData();
window.addEventListener('resize', this.resizeThrottled, false);
}
},
};
</script>
<template>
<div
class="prometheus-graphs"
v-if="!showEmptyState">
<div
class="row"
v-for="(groupData, index) in store.groups"
:key="index">
<div
class="col-md-12">
<div
class="panel panel-default prometheus-panel">
<div
class="panel-heading">
<h4>{{groupData.group}}</h4>
</div>
<div
class="panel-body">
<monitoring-row
v-for="(row, index) in groupData.metrics"
:key="index"
:row-data="row"
:update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData"
/>
</div>
</div>
</div>
</div>
</div>
<monitoring-state
:selected-state="state"
:documentation-path="documentationPath"
:settings-path="settingsPath"
v-else
/>
</template>

View file

@ -0,0 +1,291 @@
<script>
/* global Breakpoints */
import d3 from 'd3';
import monitoringLegends from './monitoring_legends.vue';
import monitoringFlag from './monitoring_flag.vue';
import monitoringDeployment from './monitoring_deployment.vue';
import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub';
import measurements from '../utils/measurements';
import { formatRelevantDigits } from '../../lib/utils/number_utils';
const bisectDate = d3.bisector(d => d.time).left;
export default {
props: {
columnData: {
type: Object,
required: true,
},
classType: {
type: String,
required: true,
},
updateAspectRatio: {
type: Boolean,
required: true,
},
deploymentData: {
type: Array,
required: true,
},
},
mixins: [MonitoringMixin],
data() {
return {
graphHeight: 500,
graphWidth: 600,
graphHeightOffset: 120,
xScale: {},
yScale: {},
margin: {},
data: [],
breakpointHandler: Breakpoints.get(),
unitOfDisplay: '',
areaColorRgb: '#8fbce8',
lineColorRgb: '#1f78d1',
yAxisLabel: '',
legendTitle: '',
reducedDeploymentData: [],
area: '',
line: '',
measurements: measurements.large,
currentData: {
time: new Date(),
value: 0,
},
currentYCoordinate: 0,
currentXCoordinate: 0,
currentFlagPosition: 0,
metricUsage: '',
showFlag: false,
showDeployInfo: true,
};
},
components: {
monitoringLegends,
monitoringFlag,
monitoringDeployment,
},
computed: {
outterViewBox() {
return `0 0 ${this.graphWidth} ${this.graphHeight}`;
},
innerViewBox() {
if ((this.graphWidth - 150) > 0) {
return `0 0 ${this.graphWidth - 150} ${this.graphHeight}`;
}
return '0 0 0 0';
},
axisTransform() {
return `translate(70, ${this.graphHeight - 100})`;
},
paddingBottomRootSvg() {
return (Math.ceil(this.graphHeight * 100) / this.graphWidth) || 0;
},
},
methods: {
draw() {
const breakpointSize = this.breakpointHandler.getBreakpointSize();
const query = this.columnData.queries[0];
this.margin = measurements.large.margin;
if (breakpointSize === 'xs' || breakpointSize === 'sm') {
this.graphHeight = 300;
this.margin = measurements.small.margin;
this.measurements = measurements.small;
}
this.data = query.result[0].values;
this.unitOfDisplay = query.unit || 'N/A';
this.yAxisLabel = this.columnData.y_axis || 'Values';
this.legendTitle = query.legend || 'Average';
this.graphWidth = this.$refs.baseSvg.clientWidth -
this.margin.left - this.margin.right;
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
if (this.data !== undefined) {
this.renderAxesPaths();
this.formatDeployments();
}
},
handleMouseOverGraph(e) {
let point = this.$refs.graphData.createSVGPoint();
point.x = e.clientX;
point.y = e.clientY;
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
point.x = point.x += 7;
const timeValueOverlay = this.xScale.invert(point.x);
const overlayIndex = bisectDate(this.data, timeValueOverlay, 1);
const d0 = this.data[overlayIndex - 1];
const d1 = this.data[overlayIndex];
if (d0 === undefined || d1 === undefined) return;
const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
this.currentData = evalTime ? d1 : d0;
this.currentXCoordinate = Math.floor(this.xScale(this.currentData.time));
const currentDeployXPos = this.mouseOverDeployInfo(point.x);
this.currentYCoordinate = this.yScale(this.currentData.value);
if (this.currentXCoordinate > (this.graphWidth - 200)) {
this.currentFlagPosition = this.currentXCoordinate - 103;
} else {
this.currentFlagPosition = this.currentXCoordinate;
}
if (currentDeployXPos) {
this.showFlag = false;
} else {
this.showFlag = true;
}
this.metricUsage = `${formatRelevantDigits(this.currentData.value)} ${this.unitOfDisplay}`;
},
renderAxesPaths() {
const axisXScale = d3.time.scale()
.range([0, this.graphWidth]);
this.yScale = d3.scale.linear()
.range([this.graphHeight - this.graphHeightOffset, 0]);
axisXScale.domain(d3.extent(this.data, d => d.time));
this.yScale.domain([0, d3.max(this.data.map(d => d.value))]);
const xAxis = d3.svg.axis()
.scale(axisXScale)
.ticks(measurements.ticks)
.orient('bottom');
const yAxis = d3.svg.axis()
.scale(this.yScale)
.ticks(measurements.ticks)
.orient('left');
d3.select(this.$refs.baseSvg).select('.x-axis').call(xAxis);
const width = this.graphWidth;
d3.select(this.$refs.baseSvg).select('.y-axis').call(yAxis)
.selectAll('.tick')
.each(function createTickLines() {
d3.select(this).select('line').attr('x2', width);
}); // This will select all of the ticks once they're rendered
this.xScale = d3.time.scale()
.range([0, this.graphWidth - 70]);
this.xScale.domain(d3.extent(this.data, d => d.time));
const areaFunction = d3.svg.area()
.x(d => this.xScale(d.time))
.y0(this.graphHeight - this.graphHeightOffset)
.y1(d => this.yScale(d.value))
.interpolate('linear');
const lineFunction = d3.svg.line()
.x(d => this.xScale(d.time))
.y(d => this.yScale(d.value));
this.line = lineFunction(this.data);
this.area = areaFunction(this.data);
},
},
watch: {
updateAspectRatio() {
if (this.updateAspectRatio) {
this.graphHeight = 500;
this.graphWidth = 600;
this.measurements = measurements.large;
this.draw();
eventHub.$emit('toggleAspectRatio');
}
},
},
mounted() {
this.draw();
},
};
</script>
<template>
<div
:class="classType">
<h5
class="text-center">
{{columnData.title}}
</h5>
<div
class="prometheus-svg-container">
<svg
:viewBox="outterViewBox"
:style="{ 'padding-bottom': paddingBottomRootSvg }"
ref="baseSvg">
<g
class="x-axis"
:transform="axisTransform">
</g>
<g
class="y-axis"
transform="translate(70, 20)">
</g>
<monitoring-legends
:graph-width="graphWidth"
:graph-height="graphHeight"
:margin="margin"
:measurements="measurements"
:area-color-rgb="areaColorRgb"
:legend-title="legendTitle"
:y-axis-label="yAxisLabel"
:metric-usage="metricUsage"
/>
<svg
class="graph-data"
:viewBox="innerViewBox"
ref="graphData">
<path
class="metric-area"
:d="area"
:fill="areaColorRgb"
transform="translate(-5, 20)">
</path>
<path
class="metric-line"
:d="line"
:stroke="lineColorRgb"
fill="none"
stroke-width="2"
transform="translate(-5, 20)">
</path>
<rect
class="prometheus-graph-overlay"
:width="(graphWidth - 70)"
:height="(graphHeight - 100)"
transform="translate(-5, 20)"
ref="graphOverlay"
@mousemove="handleMouseOverGraph($event)">
</rect>
<monitoring-deployment
:show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
<monitoring-flag
v-if="showFlag"
:current-x-coordinate="currentXCoordinate"
:current-y-coordinate="currentYCoordinate"
:current-data="currentData"
:current-flag-position="currentFlagPosition"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
</svg>
</svg>
</div>
</div>
</template>

View file

@ -0,0 +1,136 @@
<script>
import {
dateFormat,
timeFormat,
} from '../constants';
export default {
props: {
showDeployInfo: {
type: Boolean,
required: true,
},
deploymentData: {
type: Array,
required: true,
},
graphHeight: {
type: Number,
required: true,
},
graphHeightOffset: {
type: Number,
required: true,
},
},
computed: {
calculatedHeight() {
return this.graphHeight - this.graphHeightOffset;
},
},
methods: {
refText(d) {
return d.tag ? d.ref : d.sha.slice(0, 6);
},
formatTime(deploymentTime) {
return timeFormat(deploymentTime);
},
formatDate(deploymentTime) {
return dateFormat(deploymentTime);
},
nameDeploymentClass(deployment) {
return `deploy-info-${deployment.id}`;
},
transformDeploymentGroup(deployment) {
return `translate(${Math.floor(deployment.xPos) + 1}, 20)`;
},
},
};
</script>
<template>
<g
class="deploy-info"
v-if="showDeployInfo">
<g
v-for="(deployment, index) in deploymentData"
:key="index"
:class="nameDeploymentClass(deployment)"
:transform="transformDeploymentGroup(deployment)">
<rect
x="0"
y="0"
:height="calculatedHeight"
width="3"
fill="url(#shadow-gradient)">
</rect>
<line
class="deployment-line"
x1="0"
y1="0"
x2="0"
:y2="calculatedHeight"
stroke="#000">
</line>
<svg
v-if="deployment.showDeploymentFlag"
class="js-deploy-info-box"
x="3"
y="0"
width="92"
height="60">
<rect
class="rect-text-metric deploy-info-rect rect-metric"
x="1"
y="1"
rx="2"
width="90"
height="58">
</rect>
<g
transform="translate(5, 2)">
<text
class="deploy-info-text text-metric-bold">
{{refText(deployment)}}
</text>
</g>
<text
class="deploy-info-text"
y="18"
transform="translate(5, 2)">
{{formatDate(deployment.time)}}
</text>
<text
class="deploy-info-text text-metric-bold"
y="38"
transform="translate(5, 2)">
{{formatTime(deployment.time)}}
</text>
</svg>
</g>
<svg
height="0"
width="0">
<defs>
<linearGradient
id="shadow-gradient">
<stop
offset="0%"
stop-color="#000"
stop-opacity="0.4">
</stop>
<stop
offset="100%"
stop-color="#000"
stop-opacity="0">
</stop>
</linearGradient>
</defs>
</svg>
</g>
</template>

View file

@ -0,0 +1,104 @@
<script>
import {
dateFormat,
timeFormat,
} from '../constants';
export default {
props: {
currentXCoordinate: {
type: Number,
required: true,
},
currentYCoordinate: {
type: Number,
required: true,
},
currentFlagPosition: {
type: Number,
required: true,
},
currentData: {
type: Object,
required: true,
},
graphHeight: {
type: Number,
required: true,
},
graphHeightOffset: {
type: Number,
required: true,
},
},
data() {
return {
circleColorRgb: '#8fbce8',
};
},
computed: {
formatTime() {
return timeFormat(this.currentData.time);
},
formatDate() {
return dateFormat(this.currentData.time);
},
calculatedHeight() {
return this.graphHeight - this.graphHeightOffset;
},
},
};
</script>
<template>
<g class="mouse-over-flag">
<line
class="selected-metric-line"
:x1="currentXCoordinate"
:y1="0"
:x2="currentXCoordinate"
:y2="calculatedHeight"
transform="translate(-5, 20)">
</line>
<circle
class="circle-metric"
:fill="circleColorRgb"
stroke="#000"
:cx="currentXCoordinate"
:cy="currentYCoordinate"
r="5"
transform="translate(-5, 20)">
</circle>
<svg
class="rect-text-metric"
:x="currentFlagPosition"
y="0">
<rect
class="rect-metric"
x="4"
y="1"
rx="2"
width="90"
height="40"
transform="translate(-3, 20)">
</rect>
<text
class="text-metric text-metric-bold"
x="8"
y="35"
transform="translate(-5, 20)">
{{formatTime}}
</text>
<text
class="text-metric-date"
x="8"
y="15"
transform="translate(-5, 20)">
{{formatDate}}
</text>
</svg>
</g>
</template>

View file

@ -0,0 +1,144 @@
<script>
export default {
props: {
graphWidth: {
type: Number,
required: true,
},
graphHeight: {
type: Number,
required: true,
},
margin: {
type: Object,
required: true,
},
measurements: {
type: Object,
required: true,
},
areaColorRgb: {
type: String,
required: true,
},
legendTitle: {
type: String,
required: true,
},
yAxisLabel: {
type: String,
required: true,
},
metricUsage: {
type: String,
required: true,
},
},
data() {
return {
yLabelWidth: 0,
yLabelHeight: 0,
};
},
computed: {
textTransform() {
const yCoordinate = (((this.graphHeight - this.margin.top)
+ this.measurements.axisLabelLineOffset) / 2) || 0;
return `translate(15, ${yCoordinate}) rotate(-90)`;
},
rectTransform() {
const yCoordinate = ((this.graphHeight - this.margin.top) / 2)
+ (this.yLabelWidth / 2) + 10 || 0;
return `translate(0, ${yCoordinate}) rotate(-90)`;
},
xPosition() {
return (((this.graphWidth + this.measurements.axisLabelLineOffset) / 2)
- this.margin.right) || 0;
},
yPosition() {
return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0;
},
},
mounted() {
this.$nextTick(() => {
const bbox = this.$refs.ylabel.getBBox();
this.yLabelWidth = bbox.width + 10; // Added some padding
this.yLabelHeight = bbox.height + 5;
});
},
};
</script>
<template>
<g
class="axis-label-container">
<line
class="label-x-axis-line"
stroke="#000000"
stroke-width="1"
x1="10"
:y1="yPosition"
:x2="graphWidth + 20"
:y2="yPosition">
</line>
<line
class="label-y-axis-line"
stroke="#000000"
stroke-width="1"
x1="10"
y1="0"
:x2="10"
:y2="yPosition">
</line>
<rect
class="rect-axis-text"
:transform="rectTransform"
:width="yLabelWidth"
:height="yLabelHeight">
</rect>
<text
class="label-axis-text y-label-text"
text-anchor="middle"
:transform="textTransform"
ref="ylabel">
{{yAxisLabel}}
</text>
<rect
class="rect-axis-text"
:x="xPosition + 50"
:y="graphHeight - 80"
width="50"
height="50">
</rect>
<text
class="label-axis-text"
:x="xPosition + 60"
:y="yPosition"
dy=".35em">
Time
</text>
<rect
:fill="areaColorRgb"
:width="measurements.legends.width"
:height="measurements.legends.height"
x="20"
:y="graphHeight - measurements.legendOffset">
</rect>
<text
class="text-metric-title"
x="50"
:y="graphHeight - 40">
{{legendTitle}}
</text>
<text
class="text-metric-usage"
x="50"
:y="graphHeight - 25">
{{metricUsage}}
</text>
</g>
</template>

View file

@ -0,0 +1,41 @@
<script>
import monitoringColumn from './monitoring_column.vue';
export default {
props: {
rowData: {
type: Array,
required: true,
},
updateAspectRatio: {
type: Boolean,
required: true,
},
deploymentData: {
type: Array,
required: true,
},
},
components: {
monitoringColumn,
},
computed: {
bootstrapClass() {
return this.rowData.length >= 2 ? 'col-md-6' : 'col-md-12';
},
},
};
</script>
<template>
<div
class="prometheus-row row">
<monitoring-column
v-for="(column, index) in rowData"
:column-data="column"
:class-type="bootstrapClass"
:key="index"
:update-aspect-ratio="updateAspectRatio"
:deployment-data="deploymentData"
/>
</div>
</template>

View file

@ -0,0 +1,112 @@
<script>
import gettingStartedSvg from 'empty_states/monitoring/_getting_started.svg';
import loadingSvg from 'empty_states/monitoring/_loading.svg';
import unableToConnectSvg from 'empty_states/monitoring/_unable_to_connect.svg';
export default {
props: {
documentationPath: {
type: String,
required: true,
},
settingsPath: {
type: String,
required: false,
default: '',
},
selectedState: {
type: String,
required: true,
},
},
data() {
return {
states: {
gettingStarted: {
svg: gettingStartedSvg,
title: 'Get started with performance monitoring',
description: 'Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.',
buttonText: 'Configure Prometheus',
},
loading: {
svg: loadingSvg,
title: 'Waiting for performance data',
description: 'Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.',
buttonText: 'View documentation',
},
unableToConnect: {
svg: unableToConnectSvg,
title: 'Unable to connect to Prometheus server',
description: 'Ensure connectivity is available from the GitLab server to the ',
buttonText: 'View documentation',
},
},
};
},
computed: {
currentState() {
return this.states[this.selectedState];
},
buttonPath() {
if (this.selectedState === 'gettingStarted') {
return this.settingsPath;
}
return this.documentationPath;
},
showButtonDescription() {
if (this.selectedState === 'unableToConnect') return true;
return false;
},
},
};
</script>
<template>
<div
class="prometheus-state">
<div
class="row">
<div
class="col-md-4 col-md-offset-4 state-svg"
v-html="currentState.svg">
</div>
</div>
<div
class="row">
<div
class="col-md-6 col-md-offset-3">
<h4
class="text-center state-title">
{{currentState.title}}
</h4>
</div>
</div>
<div
class="row">
<div
class="col-md-6 col-md-offset-3">
<div
class="description-text text-center state-description">
{{currentState.description}}
<a
:href="settingsPath"
v-if="showButtonDescription">
Prometheus server
</a>
</div>
</div>
</div>
<div
class="row state-button-section">
<div
class="col-md-4 col-md-offset-4 text-center state-button">
<a
class="btn btn-success"
:href="buttonPath">
{{currentState.buttonText}}
</a>
</div>
</div>
</div>
</template>

View file

@ -1,211 +0,0 @@
/* global Flash */
import d3 from 'd3';
import {
dateFormat,
timeFormat,
} from './constants';
export default class Deployments {
constructor(width, height) {
this.width = width;
this.height = height;
this.endpoint = document.getElementById('js-metrics').dataset.deploymentEndpoint;
this.createGradientDef();
}
init(chartData) {
this.chartData = chartData;
this.x = d3.time.scale().range([0, this.width]);
this.x.domain(d3.extent(this.chartData, d => d.time));
this.charts = d3.selectAll('.prometheus-graph');
this.getData();
}
getData() {
$.ajax({
url: this.endpoint,
dataType: 'JSON',
})
.fail(() => new Flash('Error getting deployment information.'))
.done((data) => {
this.data = data.deployments.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at);
const xPos = Math.floor(this.x(time));
time.setSeconds(this.chartData[0].time.getSeconds());
if (xPos >= 0) {
deploymentDataArray.push({
id: deployment.id,
time,
sha: deployment.sha,
tag: deployment.tag,
ref: deployment.ref.name,
xPos,
});
}
return deploymentDataArray;
}, []);
this.plotData();
});
}
plotData() {
this.charts.each((d, i) => {
const svg = d3.select(this.charts[0][i]);
const chart = svg.select('.graph-container');
const key = svg.node().getAttribute('graph-type');
this.createLine(chart, key);
this.createDeployInfoBox(chart, key);
});
}
createGradientDef() {
const defs = d3.select('body')
.append('svg')
.attr({
height: 0,
width: 0,
})
.append('defs');
defs.append('linearGradient')
.attr({
id: 'shadow-gradient',
})
.append('stop')
.attr({
offset: '0%',
'stop-color': '#000',
'stop-opacity': 0.4,
})
.select(this.selectParentNode)
.append('stop')
.attr({
offset: '100%',
'stop-color': '#000',
'stop-opacity': 0,
});
}
createLine(chart, key) {
chart.append('g')
.attr({
class: 'deploy-info',
})
.selectAll('.deploy-info')
.data(this.data)
.enter()
.append('g')
.attr({
class: d => `deploy-info-${d.id}-${key}`,
transform: d => `translate(${Math.floor(d.xPos) + 1}, 0)`,
})
.append('rect')
.attr({
x: 1,
y: 0,
height: this.height + 1,
width: 3,
fill: 'url(#shadow-gradient)',
})
.select(this.selectParentNode)
.append('line')
.attr({
class: 'deployment-line',
x1: 0,
x2: 0,
y1: 0,
y2: this.height + 1,
});
}
createDeployInfoBox(chart, key) {
chart.selectAll('.deploy-info')
.selectAll('.js-deploy-info-box')
.data(this.data)
.enter()
.select(d => document.querySelector(`.deploy-info-${d.id}-${key}`))
.append('svg')
.attr({
class: 'js-deploy-info-box hidden',
x: 3,
y: 0,
width: 92,
height: 60,
})
.append('rect')
.attr({
class: 'rect-text-metric deploy-info-rect rect-metric',
x: 1,
y: 1,
rx: 2,
width: 90,
height: 58,
})
.select(this.selectParentNode)
.append('g')
.attr({
transform: 'translate(5, 2)',
})
.append('text')
.attr({
class: 'deploy-info-text text-metric-bold',
})
.text(Deployments.refText)
.select(this.selectParentNode)
.append('text')
.attr({
class: 'deploy-info-text',
y: 18,
})
.text(d => dateFormat(d.time))
.select(this.selectParentNode)
.append('text')
.attr({
class: 'deploy-info-text text-metric-bold',
y: 38,
})
.text(d => timeFormat(d.time));
}
static toggleDeployTextbox(deploy, key, showInfoBox) {
d3.selectAll(`.deploy-info-${deploy.id}-${key} .js-deploy-info-box`)
.classed('hidden', !showInfoBox);
}
mouseOverDeployInfo(mouseXPos, key) {
if (!this.data) return false;
let dataFound = false;
this.data.forEach((d) => {
if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) {
dataFound = d.xPos + 1;
Deployments.toggleDeployTextbox(d, key, true);
} else {
Deployments.toggleDeployTextbox(d, key, false);
}
});
return dataFound;
}
/* `this` is bound to the D3 node */
selectParentNode() {
return this.parentNode;
}
static refText(d) {
return d.tag ? d.ref : d.sha.slice(0, 6);
}
}

View file

@ -0,0 +1,3 @@
import Vue from 'vue';
export default new Vue();

View file

@ -0,0 +1,46 @@
const mixins = {
methods: {
mouseOverDeployInfo(mouseXPos) {
if (!this.reducedDeploymentData) return false;
let dataFound = false;
this.reducedDeploymentData = this.reducedDeploymentData.map((d) => {
const deployment = d;
if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) {
dataFound = d.xPos + 1;
deployment.showDeploymentFlag = true;
} else {
deployment.showDeploymentFlag = false;
}
return deployment;
});
return dataFound;
},
formatDeployments() {
this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at);
const xPos = Math.floor(this.xScale(time));
time.setSeconds(this.data[0].time.getSeconds());
if (xPos >= 0) {
deploymentDataArray.push({
id: deployment.id,
time,
sha: deployment.sha,
tag: deployment.tag,
ref: deployment.ref.name,
xPos,
showDeploymentFlag: false,
});
}
return deploymentDataArray;
}, []);
},
},
};
export default mixins;

View file

@ -1,6 +1,10 @@
import PrometheusGraph from './prometheus_graph';
import Vue from 'vue';
import Monitoring from './components/monitoring.vue';
document.addEventListener('DOMContentLoaded', function onLoad() {
document.removeEventListener('DOMContentLoaded', onLoad, false);
return new PrometheusGraph();
}, false);
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#prometheus-graphs',
components: {
'monitoring-dashboard': Monitoring,
},
render: createElement => createElement('monitoring-dashboard'),
}));

View file

@ -1,433 +0,0 @@
/* eslint-disable no-new */
/* global Flash */
import d3 from 'd3';
import statusCodes from '~/lib/utils/http_status';
import Deployments from './deployments';
import '../lib/utils/common_utils';
import { formatRelevantDigits } from '../lib/utils/number_utils';
import '../flash';
import {
dateFormat,
timeFormat,
} from './constants';
const prometheusContainer = '.prometheus-container';
const prometheusParentGraphContainer = '.prometheus-graphs';
const prometheusGraphsContainer = '.prometheus-graph';
const prometheusStatesContainer = '.prometheus-state';
const metricsEndpoint = 'metrics.json';
const bisectDate = d3.bisector(d => d.time).left;
const extraAddedWidthParent = 100;
class PrometheusGraph {
constructor() {
const $prometheusContainer = $(prometheusContainer);
const hasMetrics = $prometheusContainer.data('has-metrics');
this.docLink = $prometheusContainer.data('doc-link');
this.integrationLink = $prometheusContainer.data('prometheus-integration');
this.state = '';
$(document).ajaxError(() => {});
if (hasMetrics) {
this.margin = { top: 80, right: 180, bottom: 80, left: 100 };
this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 };
const parentContainerWidth = $(prometheusGraphsContainer).parent().width() +
extraAddedWidthParent;
this.originalWidth = parentContainerWidth;
this.originalHeight = 330;
this.width = parentContainerWidth - this.margin.left - this.margin.right;
this.height = this.originalHeight - this.margin.top - this.margin.bottom;
this.backOffRequestCounter = 0;
this.deployments = new Deployments(this.width, this.height);
this.configureGraph();
this.init();
} else {
const prevState = this.state;
this.state = '.js-getting-started';
this.updateState(prevState);
}
}
createGraph() {
Object.keys(this.graphSpecificProperties).forEach((key) => {
const value = this.graphSpecificProperties[key];
if (value.data.length > 0) {
this.plotValues(key);
}
});
}
init() {
return this.getData().then((metricsResponse) => {
let enoughData = true;
if (typeof metricsResponse === 'undefined') {
enoughData = false;
} else {
Object.keys(metricsResponse.metrics).forEach((key) => {
if (key === 'cpu_values' || key === 'memory_values') {
const currentData = (metricsResponse.metrics[key])[0];
if (currentData.values.length <= 2) {
enoughData = false;
}
}
});
}
if (enoughData) {
$(prometheusStatesContainer).hide();
$(prometheusParentGraphContainer).show();
this.transformData(metricsResponse);
this.createGraph();
const firstMetricData = this.graphSpecificProperties[
Object.keys(this.graphSpecificProperties)[0]
].data;
this.deployments.init(firstMetricData);
}
});
}
plotValues(key) {
const graphSpecifics = this.graphSpecificProperties[key];
const x = d3.time.scale()
.range([0, this.width]);
const y = d3.scale.linear()
.range([this.height, 0]);
graphSpecifics.xScale = x;
graphSpecifics.yScale = y;
const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
const chart = d3.select(prometheusGraphContainer)
.attr('width', this.width + this.margin.left + this.margin.right)
.attr('height', this.height + this.margin.bottom + this.margin.top)
.append('g')
.attr('class', 'graph-container')
.attr('transform', `translate(${this.margin.left},${this.margin.top})`);
const axisLabelContainer = d3.select(prometheusGraphContainer)
.attr('width', this.originalWidth)
.attr('height', this.originalHeight)
.append('g')
.attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`);
x.domain(d3.extent(graphSpecifics.data, d => d.time));
y.domain([0, d3.max(graphSpecifics.data.map(metricValue => metricValue.value))]);
const xAxis = d3.svg.axis()
.scale(x)
.ticks(this.commonGraphProperties.axis_no_ticks)
.orient('bottom');
const yAxis = d3.svg.axis()
.scale(y)
.ticks(this.commonGraphProperties.axis_no_ticks)
.tickSize(-this.width)
.outerTickSize(0)
.orient('left');
this.createAxisLabelContainers(axisLabelContainer, key);
chart.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0,${this.height})`)
.call(xAxis);
chart.append('g')
.attr('class', 'y-axis')
.call(yAxis);
const area = d3.svg.area()
.x(d => x(d.time))
.y0(this.height)
.y1(d => y(d.value))
.interpolate('linear');
const line = d3.svg.line()
.x(d => x(d.time))
.y(d => y(d.value));
chart.append('path')
.datum(graphSpecifics.data)
.attr('d', area)
.attr('class', 'metric-area')
.attr('fill', graphSpecifics.area_fill_color);
chart.append('path')
.datum(graphSpecifics.data)
.attr('class', 'metric-line')
.attr('stroke', graphSpecifics.line_color)
.attr('fill', 'none')
.attr('stroke-width', this.commonGraphProperties.area_stroke_width)
.attr('d', line);
// Overlay area for the mouseover events
chart.append('rect')
.attr('class', 'prometheus-graph-overlay')
.attr('width', this.width)
.attr('height', this.height)
.on('mousemove', this.handleMouseOverGraph.bind(this, prometheusGraphContainer));
}
// The legends from the metric
createAxisLabelContainers(axisLabelContainer, key) {
const graphSpecifics = this.graphSpecificProperties[key];
axisLabelContainer.append('line')
.attr('class', 'label-x-axis-line')
.attr('stroke', '#000000')
.attr('stroke-width', '1')
.attr({
x1: 10,
y1: this.originalHeight - this.margin.top,
x2: (this.originalWidth - this.margin.right) + 10,
y2: this.originalHeight - this.margin.top,
});
axisLabelContainer.append('line')
.attr('class', 'label-y-axis-line')
.attr('stroke', '#000000')
.attr('stroke-width', '1')
.attr({
x1: 10,
y1: 0,
x2: 10,
y2: this.originalHeight - this.margin.top,
});
axisLabelContainer.append('rect')
.attr('class', 'rect-axis-text')
.attr('x', 0)
.attr('y', 50)
.attr('width', 30)
.attr('height', 150);
axisLabelContainer.append('text')
.attr('class', 'label-axis-text')
.attr('text-anchor', 'middle')
.attr('transform', `translate(15, ${(this.originalHeight - this.margin.top) / 2}) rotate(-90)`)
.text(graphSpecifics.graph_legend_title);
axisLabelContainer.append('rect')
.attr('class', 'rect-axis-text')
.attr('x', (this.originalWidth / 2) - this.margin.right)
.attr('y', this.originalHeight - 100)
.attr('width', 30)
.attr('height', 80);
axisLabelContainer.append('text')
.attr('class', 'label-axis-text')
.attr('x', (this.originalWidth / 2) - this.margin.right)
.attr('y', this.originalHeight - this.margin.top)
.attr('dy', '.35em')
.text('Time');
// Legends
// Metric Usage
axisLabelContainer.append('rect')
.attr('x', this.originalWidth - 170)
.attr('y', (this.originalHeight / 2) - 60)
.style('fill', graphSpecifics.area_fill_color)
.attr('width', 20)
.attr('height', 35);
axisLabelContainer.append('text')
.attr('class', 'text-metric-title')
.attr('x', this.originalWidth - 140)
.attr('y', (this.originalHeight / 2) - 50)
.text('Average');
axisLabelContainer.append('text')
.attr('class', 'text-metric-usage')
.attr('x', this.originalWidth - 140)
.attr('y', (this.originalHeight / 2) - 25);
}
handleMouseOverGraph(prometheusGraphContainer) {
const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`);
const currentXCoordinate = d3.mouse(rectOverlay)[0];
Object.keys(this.graphSpecificProperties).forEach((key) => {
const currentGraphProps = this.graphSpecificProperties[key];
const timeValueOverlay = currentGraphProps.xScale.invert(currentXCoordinate);
const overlayIndex = bisectDate(currentGraphProps.data, timeValueOverlay, 1);
const d0 = currentGraphProps.data[overlayIndex - 1];
const d1 = currentGraphProps.data[overlayIndex];
const evalTime = timeValueOverlay - d0.time > d1.time - timeValueOverlay;
const currentData = evalTime ? d1 : d0;
const currentTimeCoordinate = Math.floor(currentGraphProps.xScale(currentData.time));
const currentDeployXPos = this.deployments.mouseOverDeployInfo(currentXCoordinate, key);
const currentPrometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
const maxValueFromData = d3.max(currentGraphProps.data.map(metricValue => metricValue.value));
const maxMetricValue = currentGraphProps.yScale(maxValueFromData);
// Clear up all the pieces of the flag
d3.selectAll(`${currentPrometheusGraphContainer} .selected-metric-line`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .circle-metric`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric:not(.deploy-info-rect)`).remove();
const currentChart = d3.select(currentPrometheusGraphContainer).select('g');
currentChart.append('line')
.attr({
class: `${currentDeployXPos ? 'hidden' : ''} selected-metric-line`,
x1: currentTimeCoordinate,
y1: currentGraphProps.yScale(0),
x2: currentTimeCoordinate,
y2: maxMetricValue,
});
currentChart.append('circle')
.attr('class', 'circle-metric')
.attr('fill', currentGraphProps.line_color)
.attr('cx', currentDeployXPos || currentTimeCoordinate)
.attr('cy', currentGraphProps.yScale(currentData.value))
.attr('r', this.commonGraphProperties.circle_radius_metric);
if (currentDeployXPos) return;
// The little box with text
const rectTextMetric = currentChart.append('svg')
.attr({
class: 'rect-text-metric',
x: currentTimeCoordinate,
y: 0,
});
rectTextMetric.append('rect')
.attr({
class: 'rect-metric',
x: 4,
y: 1,
rx: 2,
width: this.commonGraphProperties.rect_text_width,
height: this.commonGraphProperties.rect_text_height,
});
rectTextMetric.append('text')
.attr({
class: 'text-metric text-metric-bold',
x: 8,
y: 35,
})
.text(timeFormat(currentData.time));
rectTextMetric.append('text')
.attr({
class: 'text-metric-date',
x: 8,
y: 15,
})
.text(dateFormat(currentData.time));
let currentMetricValue = formatRelevantDigits(currentData.value);
if (key === 'cpu_values') {
currentMetricValue = `${currentMetricValue}%`;
} else {
currentMetricValue = `${currentMetricValue} MB`;
}
d3.select(`${currentPrometheusGraphContainer} .text-metric-usage`)
.text(currentMetricValue);
});
}
configureGraph() {
this.graphSpecificProperties = {
cpu_values: {
area_fill_color: '#edf3fc',
line_color: '#5b99f7',
graph_legend_title: 'CPU Usage (Cores)',
data: [],
xScale: {},
yScale: {},
},
memory_values: {
area_fill_color: '#fca326',
line_color: '#fc6d26',
graph_legend_title: 'Memory Usage (MB)',
data: [],
xScale: {},
yScale: {},
},
};
this.commonGraphProperties = {
area_stroke_width: 2,
median_total_characters: 8,
circle_radius_metric: 5,
rect_text_width: 90,
rect_text_height: 40,
axis_no_ticks: 3,
};
}
getData() {
const maxNumberOfRequests = 3;
this.state = '.js-loading';
this.updateState();
return gl.utils.backOff((next, stop) => {
$.ajax({
url: metricsEndpoint,
dataType: 'json',
})
.done((data, statusText, resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < maxNumberOfRequests) {
next();
} else if (this.backOffRequestCounter >= maxNumberOfRequests) {
stop(new Error('loading'));
}
} else if (!data.success) {
stop(new Error('loading'));
} else {
stop({
status: resp.status,
metrics: data,
});
}
}).fail(stop);
})
.then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
return {};
}
return resp.metrics;
})
.catch(() => {
const prevState = this.state;
this.state = '.js-unable-to-connect';
this.updateState(prevState);
});
}
transformData(metricsResponse) {
Object.keys(metricsResponse.metrics).forEach((key) => {
if (key === 'cpu_values' || key === 'memory_values') {
const metricValues = (metricsResponse.metrics[key])[0];
this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({
time: new Date(metric[0] * 1000),
value: metric[1],
}));
}
});
}
updateState(prevState) {
const $statesContainer = $(prometheusStatesContainer);
$(prometheusParentGraphContainer).hide();
if (prevState) {
$(`${prevState}`, $statesContainer).addClass('hidden');
}
$(`${this.state}`, $statesContainer).removeClass('hidden');
$(prometheusStatesContainer).show();
}
}
export default PrometheusGraph;

View file

@ -0,0 +1,19 @@
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class MonitoringService {
constructor(endpoint) {
this.graphs = Vue.resource(endpoint);
}
get() {
return this.graphs.get();
}
// eslint-disable-next-line class-methods-use-this
getDeploymentData(endpoint) {
return Vue.http.get(endpoint);
}
}

View file

@ -0,0 +1,61 @@
import _ from 'underscore';
class MonitoringStore {
constructor() {
this.groups = [];
this.deploymentData = [];
}
// eslint-disable-next-line class-methods-use-this
createArrayRows(metrics = []) {
const currentMetrics = metrics;
const availableMetrics = [];
let metricsRow = [];
let index = 1;
Object.keys(currentMetrics).forEach((key) => {
const metricValues = currentMetrics[key].queries[0].result[0].values;
if (metricValues != null) {
const literalMetrics = metricValues.map(metric => ({
time: new Date(metric[0] * 1000),
value: metric[1],
}));
currentMetrics[key].queries[0].result[0].values = literalMetrics;
metricsRow.push(currentMetrics[key]);
if (index % 2 === 0) {
availableMetrics.push(metricsRow);
metricsRow = [];
}
index = index += 1;
}
});
if (metricsRow.length > 0) {
availableMetrics.push(metricsRow);
}
return availableMetrics;
}
storeMetrics(groups = []) {
this.groups = groups.map((group) => {
const currentGroup = group;
currentGroup.metrics = _.chain(group.metrics).sortBy('weight').sortBy('title').value();
currentGroup.metrics = this.createArrayRows(currentGroup.metrics);
return currentGroup;
});
}
storeDeploymentData(deploymentData = []) {
this.deploymentData = deploymentData;
}
getMetricsCount() {
let metricsCount = 0;
this.groups.forEach((group) => {
group.metrics.forEach((metric) => {
metricsCount = metricsCount += metric.length;
});
});
return metricsCount;
}
}
export default MonitoringStore;

View file

@ -0,0 +1,39 @@
export default {
small: { // Covers both xs and sm screen sizes
margin: {
top: 40,
right: 40,
bottom: 50,
left: 40,
},
legends: {
width: 15,
height: 30,
},
backgroundLegend: {
width: 30,
height: 50,
},
axisLabelLineOffset: -20,
legendOffset: 52,
},
large: { // This covers both md and lg screen sizes
margin: {
top: 80,
right: 80,
bottom: 100,
left: 80,
},
legends: {
width: 20,
height: 35,
},
backgroundLegend: {
width: 30,
height: 150,
},
axisLabelLineOffset: 20,
legendOffset: 55,
},
ticks: 3,
};

View file

@ -829,6 +829,8 @@ export default class Notes {
*/
setupDiscussionNoteForm(dataHolder, form) {
// setup note target
const diffFileData = dataHolder.closest('.text-file');
var discussionID = dataHolder.data('discussionId');
if (discussionID) {
@ -839,9 +841,10 @@ export default class Notes {
form.attr('data-line-code', dataHolder.data('lineCode'));
form.find('#line_type').val(dataHolder.data('lineType'));
form.find('#note_noteable_type').val(dataHolder.data('noteableType'));
form.find('#note_noteable_id').val(dataHolder.data('noteableId'));
form.find('#note_commit_id').val(dataHolder.data('commitId'));
form.find('#note_noteable_type').val(diffFileData.data('noteableType'));
form.find('#note_noteable_id').val(diffFileData.data('noteableId'));
form.find('#note_commit_id').val(diffFileData.data('commitId'));
form.find('#note_type').val(dataHolder.data('noteType'));
// LegacyDiffNote
@ -1485,7 +1488,7 @@ export default class Notes {
const cachedNoteBodyText = $noteBodyText.html();
// Show updated comment content temporarily
$noteBodyText.html(_.escape(formContent));
$noteBodyText.html(formContent);
$editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half');
$editingNote.find('.note-headline-meta a').html('<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>');

View file

@ -1,148 +0,0 @@
import Vue from 'vue';
import Translate from '../../vue_shared/translate';
Vue.use(Translate);
const inputNameAttribute = 'schedule[cron]';
export default {
props: {
initialCronInterval: {
type: String,
required: false,
default: '',
},
},
data() {
return {
inputNameAttribute,
cronInterval: this.initialCronInterval,
cronIntervalPresets: {
everyDay: '0 4 * * *',
everyWeek: '0 4 * * 0',
everyMonth: '0 4 1 * *',
},
cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron',
customInputEnabled: false,
};
},
computed: {
intervalIsPreset() {
return _.contains(this.cronIntervalPresets, this.cronInterval);
},
// The text input is editable when there's a custom interval, or when it's
// a preset interval and the user clicks the 'custom' radio button
isEditable() {
return !!(this.customInputEnabled || !this.intervalIsPreset);
},
},
methods: {
toggleCustomInput(shouldEnable) {
this.customInputEnabled = shouldEnable;
if (shouldEnable) {
// We need to change the value so other radios don't remain selected
// because the model (cronInterval) hasn't changed. The server trims it.
this.cronInterval = `${this.cronInterval} `;
}
},
},
created() {
if (this.intervalIsPreset) {
this.enableCustomInput = false;
}
},
watch: {
cronInterval() {
// updates field validation state when model changes, as
// glFieldError only updates on input.
Vue.nextTick(() => {
gl.pipelineScheduleFieldErrors.updateFormValidityState();
});
},
},
template: `
<div class="interval-pattern-form-group">
<div class="cron-preset-radio-input">
<input
id="custom"
class="label-light"
type="radio"
:name="inputNameAttribute"
:value="cronInterval"
:checked="isEditable"
@click="toggleCustomInput(true)"
/>
<label for="custom">
{{ s__('PipelineSheduleIntervalPattern|Custom') }}
</label>
<span class="cron-syntax-link-wrap">
(<a :href="cronSyntaxUrl" target="_blank">{{ __('Cron syntax') }}</a>)
</span>
</div>
<div class="cron-preset-radio-input">
<input
id="every-day"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyDay"
@click="toggleCustomInput(false)"
/>
<label class="label-light" for="every-day">
{{ __('Every day (at 4:00am)') }}
</label>
</div>
<div class="cron-preset-radio-input">
<input
id="every-week"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyWeek"
@click="toggleCustomInput(false)"
/>
<label class="label-light" for="every-week">
{{ __('Every week (Sundays at 4:00am)') }}
</label>
</div>
<div class="cron-preset-radio-input">
<input
id="every-month"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyMonth"
@click="toggleCustomInput(false)"
/>
<label class="label-light" for="every-month">
{{ __('Every month (on the 1st at 4:00am)') }}
</label>
</div>
<div class="cron-interval-input-wrapper">
<input
id="schedule_cron"
class="form-control inline cron-interval-input"
type="text"
:placeholder="__('Define a custom pattern with cron syntax')"
required="true"
v-model="cronInterval"
:name="inputNameAttribute"
:disabled="!isEditable"
/>
</div>
</div>
`,
};

View file

@ -0,0 +1,144 @@
<script>
export default {
props: {
initialCronInterval: {
type: String,
required: false,
default: '',
},
},
data() {
return {
inputNameAttribute: 'schedule[cron]',
cronInterval: this.initialCronInterval,
cronIntervalPresets: {
everyDay: '0 4 * * *',
everyWeek: '0 4 * * 0',
everyMonth: '0 4 1 * *',
},
cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron',
customInputEnabled: false,
};
},
computed: {
intervalIsPreset() {
return _.contains(this.cronIntervalPresets, this.cronInterval);
},
// The text input is editable when there's a custom interval, or when it's
// a preset interval and the user clicks the 'custom' radio button
isEditable() {
return !!(this.customInputEnabled || !this.intervalIsPreset);
},
},
methods: {
toggleCustomInput(shouldEnable) {
this.customInputEnabled = shouldEnable;
if (shouldEnable) {
// We need to change the value so other radios don't remain selected
// because the model (cronInterval) hasn't changed. The server trims it.
this.cronInterval = `${this.cronInterval} `;
}
},
},
created() {
if (this.intervalIsPreset) {
this.enableCustomInput = false;
}
},
watch: {
cronInterval() {
// updates field validation state when model changes, as
// glFieldError only updates on input.
this.$nextTick(() => {
gl.pipelineScheduleFieldErrors.updateFormValidityState();
});
},
},
};
</script>
<template>
<div class="interval-pattern-form-group">
<div class="cron-preset-radio-input">
<input
id="custom"
class="label-light"
type="radio"
:name="inputNameAttribute"
:value="cronInterval"
:checked="isEditable"
@click="toggleCustomInput(true)"
/>
<label for="custom">
{{ s__('PipelineSheduleIntervalPattern|Custom') }}
</label>
<span class="cron-syntax-link-wrap">
(<a :href="cronSyntaxUrl" target="_blank">{{ __('Cron syntax') }}</a>)
</span>
</div>
<div class="cron-preset-radio-input">
<input
id="every-day"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyDay"
@click="toggleCustomInput(false)"
/>
<label class="label-light" for="every-day">
{{ __('Every day (at 4:00am)') }}
</label>
</div>
<div class="cron-preset-radio-input">
<input
id="every-week"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyWeek"
@click="toggleCustomInput(false)"
/>
<label class="label-light" for="every-week">
{{ __('Every week (Sundays at 4:00am)') }}
</label>
</div>
<div class="cron-preset-radio-input">
<input
id="every-month"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyMonth"
@click="toggleCustomInput(false)"
/>
<label class="label-light" for="every-month">
{{ __('Every month (on the 1st at 4:00am)') }}
</label>
</div>
<div class="cron-interval-input-wrapper">
<input
id="schedule_cron"
class="form-control inline cron-interval-input"
type="text"
:placeholder="__('Define a custom pattern with cron syntax')"
required="true"
v-model="cronInterval"
:name="inputNameAttribute"
:disabled="!isEditable"
/>
</div>
</div>
</template>

View file

@ -1,20 +1,41 @@
import Vue from 'vue';
import IntervalPatternInput from './components/interval_pattern_input';
import Translate from '../vue_shared/translate';
import intervalPatternInput from './components/interval_pattern_input.vue';
import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown';
document.addEventListener('DOMContentLoaded', () => {
const IntervalPatternInputComponent = Vue.extend(IntervalPatternInput);
Vue.use(Translate);
function initIntervalPatternInput() {
const intervalPatternMount = document.getElementById('interval-pattern-input');
const initialCronInterval = intervalPatternMount ? intervalPatternMount.dataset.initialInterval : '';
new IntervalPatternInputComponent({
propsData: {
initialCronInterval,
return new Vue({
el: intervalPatternMount,
components: {
intervalPatternInput,
},
}).$mount(intervalPatternMount);
render(createElement) {
return createElement('interval-pattern-input', {
props: {
initialCronInterval,
},
});
},
});
}
document.addEventListener('DOMContentLoaded', () => {
/* Most of the form is written in haml, but for fields with more complex behaviors,
* you should mount individual Vue components here. If at some point components need
* to share state, it may make sense to refactor the whole form to Vue */
initIntervalPatternInput();
// Initialize non-Vue JS components in the form
const formElement = document.getElementById('new-pipeline-schedule-form');
gl.timezoneDropdown = new TimezoneDropdown();
gl.targetBranchDropdown = new TargetBranchDropdown();
gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement);

View file

@ -3,7 +3,7 @@
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
@ -28,12 +28,12 @@ export default {
required: false,
},
},
directives: {
tooltip,
},
components: {
loadingIcon,
},
mixins: [
tooltipMixin,
],
data() {
return {
isLoading: false,
@ -58,7 +58,6 @@ export default {
makeRequest() {
this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
eventHub.$emit('postAction', this.endpoint);
},
},
@ -67,6 +66,7 @@ export default {
<template>
<button
v-tooltip
type="button"
@click="onClick"
:class="buttonClass"
@ -74,7 +74,6 @@ export default {
:aria-label="title"
data-container="body"
data-placement="top"
ref="tooltip"
:disabled="isLoading">
<i
:class="iconClass"

View file

@ -1,6 +1,6 @@
<script>
import getActionIcon from '../../../vue_shared/ci_action_icons';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
@ -29,9 +29,9 @@
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
computed: {
actionIconSvg() {
@ -46,12 +46,11 @@
</script>
<template>
<a
v-tooltip
:data-method="actionMethod"
:title="tooltipText"
:href="link"
ref="tooltip"
class="ci-action-icon-container"
data-toggle="tooltip"
data-container="body">
<i

View file

@ -1,6 +1,6 @@
<script>
import getActionIcon from '../../../vue_shared/ci_action_icons';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
@ -29,9 +29,9 @@
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
computed: {
actionIconSvg() {
@ -42,13 +42,12 @@
</script>
<template>
<a
v-tooltip
:data-method="actionMethod"
:title="tooltipText"
:href="link"
ref="tooltip"
rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon"
data-toggle="tooltip"
data-container="body"
v-html="actionIconSvg"
aria-label="Job's action">

View file

@ -1,7 +1,7 @@
<script>
import jobNameComponent from './job_name_component.vue';
import jobComponent from './job_component.vue';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders the dropdown for the pipeline graph.
@ -34,9 +34,9 @@
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
components: {
jobComponent,
@ -53,12 +53,12 @@
<template>
<div>
<button
v-tooltip
type="button"
data-toggle="dropdown"
data-container="body"
class="dropdown-menu-toggle build-content"
:title="tooltipText"
ref="tooltip">
:title="tooltipText">
<job-name-component
:name="job.name"

View file

@ -2,7 +2,7 @@
import actionComponent from './action_component.vue';
import dropdownActionComponent from './dropdown_action_component.vue';
import jobNameComponent from './job_name_component.vue';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
@ -54,9 +54,9 @@
jobNameComponent,
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
computed: {
tooltipText() {
@ -77,12 +77,11 @@
<template>
<div>
<a
v-tooltip
v-if="job.status.details_path"
:href="job.status.details_path"
:title="tooltipText"
:class="cssClassJobName"
ref="tooltip"
data-toggle="tooltip"
data-container="body">
<job-name-component
@ -93,10 +92,9 @@
<div
v-else
v-tooltip
:title="tooltipText"
:class="cssClassJobName"
ref="tooltip"
data-toggle="tooltip"
data-container="body">
<job-name-component

View file

@ -1,6 +1,6 @@
<script>
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
@ -12,9 +12,9 @@ export default {
components: {
userAvatarLink,
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
computed: {
user() {
return this.pipeline.user;
@ -45,16 +45,16 @@ export default {
<div class="label-container">
<span
v-if="pipeline.flags.latest"
v-tooltip
class="js-pipeline-url-latest label label-success"
title="Latest pipeline for this branch"
ref="tooltip">
title="Latest pipeline for this branch">
latest
</span>
<span
v-if="pipeline.flags.yaml_errors"
v-tooltip
class="js-pipeline-url-yaml label label-danger"
:title="pipeline.yaml_errors"
ref="tooltip">
:title="pipeline.yaml_errors">
yaml invalid
</span>
<span

View file

@ -4,6 +4,7 @@
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
@ -12,6 +13,9 @@
required: true,
},
},
directives: {
tooltip,
},
components: {
loadingIcon,
},
@ -25,8 +29,6 @@
onClickAction(endpoint) {
this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
eventHub.$emit('postAction', endpoint);
},
@ -43,13 +45,13 @@
<template>
<div class="btn-group">
<button
v-tooltip
type="button"
class="dropdown-new btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions"
title="Manual job"
data-toggle="dropdown"
data-placement="top"
aria-label="Manual job"
ref="tooltip"
:disabled="isLoading">
<span v-html="playIconSvg"></span>
<i

View file

@ -1,5 +1,5 @@
<script>
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
@ -8,9 +8,9 @@
required: true,
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
};
</script>
<template>
@ -18,12 +18,12 @@
class="btn-group"
role="group">
<button
v-tooltip
class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
title="Artifacts"
data-placement="top"
data-toggle="dropdown"
aria-label="Artifacts"
ref="tooltip">
aria-label="Artifacts">
<i
class="fa fa-download"
aria-hidden="true">

View file

@ -16,7 +16,7 @@
/* global Flash */
import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
@ -32,15 +32,14 @@ export default {
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
data() {
return {
isLoading: false,
dropdownContent: '',
endpoint: this.stage.dropdown_path,
};
},
@ -73,7 +72,7 @@ export default {
},
fetchJobs() {
this.$http.get(this.endpoint)
this.$http.get(this.stage.dropdown_path)
.then((response) => {
this.dropdownContent = response.json().html;
this.isLoading = false;
@ -132,7 +131,7 @@ export default {
<template>
<div class="dropdown">
<button
ref="tooltip"
v-tooltip
:class="triggerButtonClass"
@click="onClickStage"
class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button"

View file

@ -1,7 +1,7 @@
<script>
import iconTimerSvg from 'icons/_icon_timer.svg';
import '../../lib/utils/datetime_utility';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago';
export default {
@ -16,9 +16,11 @@
},
},
mixins: [
tooltipMixin,
timeagoMixin,
],
directives: {
tooltip,
},
data() {
return {
iconTimerSvg,
@ -81,7 +83,7 @@
</i>
<time
ref="tooltip"
v-tooltip
data-placement="top"
data-container="body"
:title="tooltipTitle(finishedTime)">

View file

@ -10,6 +10,8 @@ import Cookies from 'js-cookie';
this.$sidebarInner = this.sidebar.find('.issuable-sidebar');
this.$navGitlab = $('.navbar-gitlab');
this.$layoutNav = $('.layout-nav');
this.$subScroll = $('.sub-nav-scroll');
this.$rightSidebar = $('.js-right-sidebar');
this.removeListeners();
@ -27,14 +29,14 @@ import Cookies from 'js-cookie';
Sidebar.prototype.addEventListeners = function() {
const $document = $(document);
const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 20);
const debouncedSetSidebarHeight = _.debounce(this.setSidebarHeight.bind(this), 200);
const slowerThrottledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 200);
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
$(window).on('resize', () => throttledSetSidebarHeight());
$document.on('scroll', () => debouncedSetSidebarHeight());
$document.on('scroll', () => slowerThrottledSetSidebarHeight());
$document.on('click', '.js-sidebar-toggle', function(e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault();
@ -213,7 +215,7 @@ import Cookies from 'js-cookie';
};
Sidebar.prototype.setSidebarHeight = function() {
const $navHeight = this.$navGitlab.outerHeight();
const $navHeight = this.$navGitlab.outerHeight() + this.$layoutNav.outerHeight() + (this.$subScroll ? this.$subScroll.outerHeight() : 0);
const diff = $navHeight - $(window).scrollTop();
if (diff > 0) {
this.$rightSidebar.outerHeight($(window).height() - diff);

View file

@ -14,6 +14,11 @@ export default {
type: Boolean,
required: true,
},
showToggle: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
assigneeTitle() {
@ -36,6 +41,19 @@ export default {
>
Edit
</a>
<a
v-if="showToggle"
aria-label="Toggle sidebar"
class="gutter-toggle pull-right js-sidebar-toggle"
href="#"
role="button"
>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-angle-double-right"
/>
</a>
</div>
`,
};

View file

@ -64,6 +64,7 @@ export default {
},
beforeMount() {
this.field = this.$el.dataset.field;
this.signedIn = typeof this.$el.dataset.signedIn !== 'undefined';
},
template: `
<div>
@ -71,6 +72,7 @@ export default {
:number-of-assignees="store.assignees.length"
:loading="loading || store.isFetching.assignees"
:editable="store.editable"
:show-toggle="!signedIn"
/>
<assignees
v-if="!store.isFetching.assignees"

View file

@ -1,5 +1,7 @@
/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
import FilesCommentButton from './files_comment_button';
(function() {
window.SingleFileDiff = (function() {
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
@ -78,6 +80,8 @@
gl.diffNotesCompileComponents();
}
FilesCommentButton.init($(_this.file));
if (cb) cb();
};
})(this));

View file

@ -17,6 +17,9 @@ export default {
return hasCI && !ciStatus;
},
hasPipeline() {
return Object.keys(this.mr.pipeline || {}).length > 0;
},
svg() {
return statusIconEntityMap.icon_status_failed;
},
@ -30,7 +33,11 @@ export default {
template: `
<div class="mr-widget-heading">
<div class="ci-widget">
<template v-if="hasCIError">
<template v-if="!hasPipeline">
<i class="fa fa-spinner fa-spin append-right-10" aria-hidden="true"></i>
Waiting for pipeline...
</template>
<template v-else-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error">
<span class="js-icon-link icon-link">
<span

View file

@ -2,7 +2,7 @@
import ciIconBadge from './ci_badge_link.vue';
import loadingIcon from './loading_icon.vue';
import timeagoTooltip from './time_ago_tooltip.vue';
import tooltipMixin from '../mixins/tooltip';
import tooltip from '../directives/tooltip';
import userAvatarImage from './user_avatar/user_avatar_image.vue';
/**
@ -47,9 +47,9 @@ export default {
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
components: {
ciIconBadge,
@ -90,10 +90,10 @@ export default {
<template v-if="user">
<a
v-tooltip
:href="user.path"
:title="user.email"
class="js-user-link commit-committer-link"
ref="tooltip">
class="js-user-link commit-committer-link">
<user-avatar-image
:img-src="user.avatar_url"

View file

@ -12,9 +12,18 @@
required: false,
default: '1',
},
inline: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
rootElementType() {
return this.inline ? 'span' : 'div';
},
cssClass() {
return `fa-${this.size}x`;
},
@ -22,12 +31,14 @@
};
</script>
<template>
<div class="text-center">
<component
:is="this.rootElementType"
class="text-center">
<i
class="fa fa-spin fa-spinner"
:class="cssClass"
aria-hidden="true"
:aria-label="label">
</i>
</div>
</component>
</template>

View file

@ -64,6 +64,12 @@
*/
return new gl.GLForm($(this.$refs['gl-form']), true);
},
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('gl-form');
if (glForm) {
glForm.destroy();
}
},
};
</script>

View file

@ -1,17 +1,17 @@
<script>
import tooltipMixin from '../../mixins/tooltip';
import tooltip from '../../directives/tooltip';
import toolbarButton from './toolbar_button.vue';
export default {
mixins: [
tooltipMixin,
],
props: {
previewMarkdown: {
type: Boolean,
required: true,
},
},
directives: {
tooltip,
},
components: {
toolbarButton,
},
@ -94,13 +94,13 @@
</div>
<div class="toolbar-group">
<button
v-tooltip
aria-label="Go full screen"
class="toolbar-btn js-zen-enter"
data-container="body"
tabindex="-1"
title="Go full screen"
type="button"
ref="tooltip">
type="button">
<i
aria-hidden="true"
class="fa fa-arrows-alt fa-fw">

View file

@ -1,10 +1,7 @@
<script>
import tooltipMixin from '../../mixins/tooltip';
import tooltip from '../../directives/tooltip';
export default {
mixins: [
tooltipMixin,
],
props: {
buttonTitle: {
type: String,
@ -29,6 +26,9 @@
default: false,
},
},
directives: {
tooltip,
},
computed: {
iconClass() {
return `fa-${this.icon}`;
@ -39,10 +39,10 @@
<template>
<button
v-tooltip
type="button"
class="toolbar-btn js-md hidden-xs"
tabindex="-1"
ref="tooltip"
data-container="body"
:data-md-tag="tag"
:data-md-block="tagBlock"

View file

@ -1,5 +1,5 @@
<script>
import tooltipMixin from '../mixins/tooltip';
import tooltip from '../directives/tooltip';
import timeagoMixin from '../mixins/timeago';
import '../../lib/utils/datetime_utility';
@ -28,19 +28,21 @@ export default {
},
mixins: [
tooltipMixin,
timeagoMixin,
],
directives: {
tooltip,
},
};
</script>
<template>
<time
v-tooltip
:class="cssClass"
class="js-vue-timeago"
:title="tooltipTitle(time)"
:data-placement="tooltipPlacement"
data-container="body"
ref="tooltip">
data-container="body">
{{timeFormated(time)}}
</time>
</template>

View file

@ -16,11 +16,10 @@
*/
import defaultAvatarUrl from 'images/no_avatar.png';
import TooltipMixin from '../../mixins/tooltip';
import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarImage',
mixins: [TooltipMixin],
props: {
imgSrc: {
type: String,
@ -53,6 +52,9 @@ export default {
default: 'top',
},
},
directives: {
tooltip,
},
computed: {
tooltipContainer() {
return this.tooltipText ? 'body' : null;
@ -72,6 +74,7 @@ export default {
<template>
<img
v-tooltip
class="avatar"
:class="[avatarSizeClass, cssClasses]"
:src="imageSource"
@ -81,6 +84,5 @@ export default {
:data-container="tooltipContainer"
:data-placement="tooltipPlacement"
:title="tooltipText"
ref="tooltip"
/>
</template>

View file

@ -0,0 +1,13 @@
export default {
bind(el) {
$(el).tooltip();
},
componentUpdated(el) {
$(el).tooltip('fixTitle');
},
unbind(el) {
$(el).tooltip('destroy');
},
};

View file

@ -1,13 +0,0 @@
export default {
mounted() {
$(this.$refs.tooltip).tooltip();
},
updated() {
$(this.$refs.tooltip).tooltip('fixTitle');
},
beforeDestroy() {
$(this.$refs.tooltip).tooltip('destroy');
},
};

View file

@ -129,7 +129,7 @@
}
/**
* Annotate file
* Blame file
*/
&.blame {
table {

View file

@ -34,6 +34,8 @@ header {
top: 0;
left: 0;
right: 0;
color: $gl-text-color-secondary;
border-radius: 0;
@media (max-width: $screen-xs-min) {
padding: 0 16px;
@ -59,7 +61,7 @@ header {
padding: 0;
.nav > li > a {
color: $gl-text-color-secondary;
color: currentColor;
font-size: 18px;
padding: 0;
margin: (($header-height - 28) / 2) 3px;
@ -84,7 +86,7 @@ header {
&:hover,
&:focus,
&:active {
background-color: $gray-light;
background-color: transparent;
color: $gl-text-color;
svg {
@ -96,13 +98,19 @@ header {
font-size: 14px;
}
.fa-chevron-down {
position: relative;
top: -3px;
font-size: 10px;
}
svg {
position: relative;
top: 2px;
height: 17px;
// hack to get SVG to line up with FA icons
width: 23px;
fill: $gl-text-color-secondary;
fill: currentColor;
}
}
@ -225,7 +233,7 @@ header {
}
a {
color: $gl-text-color;
color: currentColor;
&:hover {
text-decoration: underline;
@ -346,6 +354,7 @@ header {
width: auto;
min-width: 140px;
margin-top: -5px;
color: $gl-text-color;
left: auto;
.current-user {

View file

@ -74,6 +74,12 @@ $red-700: #a62d19;
$red-800: #8b2615;
$red-900: #711e11;
$purple-600: #6e49cb;
$purple-650: #5c35ae;
$purple-700: #4a2192;
$purple-800: #2c0a5c;
$purple-900: #380d75;
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
@ -99,6 +105,7 @@ $well-light-text-color: #5b6169;
*/
$gl-font-size: 14px;
$gl-text-color: rgba(0, 0, 0, .85);
$gl-text-color-light: rgba(0, 0, 0, .7);
$gl-text-color-secondary: rgba(0, 0, 0, .55);
$gl-text-color-disabled: rgba(0, 0, 0, .35);
$gl-text-color-inverted: rgba(255, 255, 255, 1.0);

View file

@ -0,0 +1,266 @@
@import "framework/variables";
@import 'framework/tw_bootstrap_variables';
@import "bootstrap/variables";
header.navbar-gitlab-new {
color: $white-light;
background-color: $purple-900;
border-bottom: 0;
.header-content {
padding-left: 0;
.title-container {
align-items: stretch;
padding-top: 0;
overflow: visible;
}
.title {
display: flex;
padding-right: 0;
color: currentColor;
> a {
display: flex;
align-items: center;
padding-top: 3px;
padding-right: $gl-padding;
padding-left: $gl-padding;
margin-left: -$gl-padding;
border-bottom: 3px solid transparent;
@media (min-width: $screen-sm-min) {
padding-right: $gl-padding;
padding-left: $gl-padding;
}
svg {
margin-top: -3px;
@media (min-width: $screen-sm-min) {
margin-right: 10px;
}
}
&:hover,
&:focus {
color: currentColor;
text-decoration: none;
border-bottom-color: $white-light;
}
}
}
.dropdown.open {
> a {
border-bottom-color: $white-light;
}
}
.dropdown-menu {
margin-top: 4px;
min-width: 130px;
@media (max-width: $screen-xs-max) {
left: auto;
right: 0;
}
}
}
.navbar-collapse {
padding-left: 0;
color: $white-light;
box-shadow: 0;
@media (max-width: $screen-xs-max) {
margin-left: -$gl-padding;
margin-right: -10px;
}
.dropdown-bold-header {
color: initial;
}
.nav {
> li:not(.hidden-xs) a {
@media (max-width: $screen-xs-max) {
margin-left: 0;
min-width: 100%;
}
}
}
}
.container-fluid {
.navbar-toggle {
min-width: 45px;
padding: 6px $gl-padding;
margin-right: -7px;
font-size: 14px;
text-align: center;
color: currentColor;
border-left: 1px solid lighten($purple-700, 10%);
&:hover,
&:focus,
&.active {
color: currentColor;
background-color: transparent;
}
}
.navbar-nav {
@media (max-width: $screen-xs-max) {
display: flex;
padding-right: 10px;
}
li {
.badge {
box-shadow: none;
}
}
}
.nav > li {
&.header-user {
@media (max-width: $screen-xs-max) {
padding-left: 10px;
}
}
> a {
background: none;
opacity: .9;
will-change: opacity;
&.header-user-dropdown-toggle {
.header-user-avatar {
border-color: $white-light;
}
}
&:hover,
&:focus {
color: $white-light;
opacity: 1;
> svg {
fill: $white-light;
}
&.header-user-dropdown-toggle {
.header-user-avatar {
border-color: $white-light;
}
}
}
}
}
}
}
.navbar-sub-nav {
display: flex;
margin-bottom: 0;
color: $white-light;
> li {
&.active > a,
a:hover,
a:focus {
border-bottom-color: $white-light;
text-decoration: none;
outline: 0;
opacity: 1;
}
> a {
display: block;
padding: 16px 10px 13px;
font-size: 13px;
color: currentColor;
border-bottom: 3px solid transparent;
opacity: .9;
will-change: opacity;
@media (min-width: $screen-sm-min) {
padding: 15px $gl-padding 12px;
font-size: 14px;
}
}
}
.dropdown-chevron {
position: relative;
top: -1px;
font-size: 10px;
}
}
.header-user .dropdown-menu-nav,
.header-new .dropdown-menu-nav {
margin-top: 4px;
}
.search {
form {
border-color: $purple-800;
&:hover {
border-color: rgba($white-light, .6);
box-shadow: none;
}
}
&.search-active form {
border-color: $white-light;
}
form,
.search-input {
background-color: $purple-700;
}
.search-input {
color: $white-light;
}
.search-input::placeholder {
color: rgba($white-light, .6);
}
.location-badge {
font-size: 12px;
color: rgba($white-light, .6);
background-color: $purple-800;
transition: color 0.15s;
will-change: color;
}
.search-input-wrap {
.search-icon,
.clear-icon {
color: rgba($white-light, .6);
}
}
&.search-active {
.location-badge {
color: $white-light;
background-color: $purple-800;
}
.search-input-wrap {
.search-icon {
color: rgba($white-light, .6);
}
.clear-icon {
color: $white-light;
}
}
}
}

View file

@ -0,0 +1,150 @@
@import "framework/variables";
@import 'framework/tw_bootstrap_variables';
@import "bootstrap/variables";
$new-sidebar-width: 220px;
.page-with-new-sidebar {
@media (min-width: $screen-sm-min) {
padding-left: $new-sidebar-width;
}
// Override position: absolute
.right-sidebar {
position: fixed;
height: 100%;
}
}
.context-header {
background-color: $gray-normal;
border-bottom: 1px solid $border-color;
font-weight: 600;
display: flex;
align-items: center;
padding: 10px 14px;
.avatar-container {
flex: 0 0 40px;
}
&:hover {
background-color: $border-color;
}
}
.settings-avatar {
background-color: $white-light;
i {
font-size: 20px;
width: 100%;
color: $gl-text-color-secondary;
text-align: center;
align-self: center;
}
}
.nav-sidebar {
position: fixed;
z-index: 400;
width: $new-sidebar-width;
top: 50px;
bottom: 0;
left: 0;
overflow: auto;
background-color: $gray-light;
border-right: 1px solid $border-color;
ul {
padding: 0;
list-style: none;
}
li {
a {
display: block;
padding: 12px 14px;
}
}
a {
color: $gl-text-color;
text-decoration: none;
}
}
.sidebar-sub-level-items {
display: none;
> li {
a {
padding: 12px 24px;
color: $gl-text-color-light;
&:hover {
color: $gl-text-color;
background-color: $border-color;
}
}
&.active {
> a {
color: $purple-650;
font-weight: 600;
}
}
}
}
.sidebar-top-level-items {
> li {
.badge {
float: right;
background-color: $border-color;
color: $gl-text-color;
}
&.active {
> a {
background-color: $purple-600;
color: $white-light;
font-weight: 600;
}
.badge {
background-color: $purple-700;
color: $white-light;
}
.sidebar-sub-level-items {
background-color: $gray-normal;
border-left: 6px solid $purple-600;
display: block;
}
}
&:not(.active) > a:hover {
background-color: $border-color;
.badge {
transition: background-color 100ms linear;
background-color: $gray-normal;
}
}
}
}
// Make issue boards full-height now that sub-nav is gone
.boards-list {
height: calc(100vh - 50px);
@media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS
// scss-lint:disable DuplicateProperty
height: calc(100vh - 120px);
// scss-lint:enable DuplicateProperty
}
}

View file

@ -147,10 +147,9 @@
top: 35px;
left: 10px;
bottom: 0;
overflow-y: scroll;
overflow-x: hidden;
padding: 10px 20px 20px 5px;
white-space: pre;
white-space: pre-wrap;
overflow: auto;
}
.environment-information {
@ -399,6 +398,7 @@
.build-light-text {
color: $gl-text-color-secondary;
word-wrap: break-word;
}
.build-gutter-toggle {

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