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:
commit
5dedea358d
844 changed files with 20562 additions and 7991 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
228
CHANGELOG.md
228
CHANGELOG.md
|
@ -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)
|
||||
- Don’t 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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1 +1 @@
|
|||
5.0.5
|
||||
5.0.6
|
||||
|
|
5
Gemfile
5
Gemfile
|
@ -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'
|
||||
|
|
25
Gemfile.lock
25
Gemfile.lock
|
@ -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)
|
||||
|
||||
|
|
BIN
app/assets/images/new_nav.png
Normal file
BIN
app/assets/images/new_nav.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
BIN
app/assets/images/old_nav.png
Normal file
BIN
app/assets/images/old_nav.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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':
|
||||
|
|
99
app/assets/javascripts/emoji/index.js
Normal file
99
app/assets/javascripts/emoji/index.js
Normal 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>
|
||||
`;
|
||||
}
|
10
app/assets/javascripts/emoji/support/index.js
Normal file
10
app/assets/javascripts/emoji/support/index.js
Normal 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);
|
||||
}
|
|
@ -111,7 +111,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe
|
|||
}
|
||||
|
||||
export {
|
||||
isEmojiUnicodeSupported,
|
||||
isEmojiUnicodeSupported as default,
|
||||
isFlagEmoji,
|
||||
isKeycapEmoji,
|
||||
isSkinToneComboEmoji,
|
|
@ -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,
|
||||
};
|
|
@ -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">
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
11
app/assets/javascripts/experimental_flags.js
Normal file
11
app/assets/javascripts/experimental_flags.js
Normal 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,
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
|
7
app/assets/javascripts/lib/utils/dom_utils.js
Normal file
7
app/assets/javascripts/lib/utils/dom_utils.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
export const addClassIfElementExists = (element, className) => {
|
||||
if (element) {
|
||||
element.classList.add(className);
|
||||
}
|
||||
};
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
|
||||
|
|
157
app/assets/javascripts/monitoring/components/monitoring.vue
Normal file
157
app/assets/javascripts/monitoring/components/monitoring.vue
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
104
app/assets/javascripts/monitoring/components/monitoring_flag.vue
Normal file
104
app/assets/javascripts/monitoring/components/monitoring_flag.vue
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
3
app/assets/javascripts/monitoring/event_hub.js
Normal file
3
app/assets/javascripts/monitoring/event_hub.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
export default new Vue();
|
|
@ -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;
|
|
@ -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'),
|
||||
}));
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
61
app/assets/javascripts/monitoring/stores/monitoring_store.js
Normal file
61
app/assets/javascripts/monitoring/stores/monitoring_store.js
Normal 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;
|
39
app/assets/javascripts/monitoring/utils/measurements.js
Normal file
39
app/assets/javascripts/monitoring/utils/measurements.js
Normal 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,
|
||||
};
|
|
@ -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>');
|
||||
|
||||
|
|
|
@ -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>
|
||||
`,
|
||||
};
|
|
@ -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>
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)">
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
`,
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
13
app/assets/javascripts/vue_shared/directives/tooltip.js
Normal file
13
app/assets/javascripts/vue_shared/directives/tooltip.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
export default {
|
||||
bind(el) {
|
||||
$(el).tooltip();
|
||||
},
|
||||
|
||||
componentUpdated(el) {
|
||||
$(el).tooltip('fixTitle');
|
||||
},
|
||||
|
||||
unbind(el) {
|
||||
$(el).tooltip('destroy');
|
||||
},
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
export default {
|
||||
mounted() {
|
||||
$(this.$refs.tooltip).tooltip();
|
||||
},
|
||||
|
||||
updated() {
|
||||
$(this.$refs.tooltip).tooltip('fixTitle');
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
$(this.$refs.tooltip).tooltip('destroy');
|
||||
},
|
||||
};
|
|
@ -129,7 +129,7 @@
|
|||
}
|
||||
|
||||
/**
|
||||
* Annotate file
|
||||
* Blame file
|
||||
*/
|
||||
&.blame {
|
||||
table {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
266
app/assets/stylesheets/new_nav.scss
Normal file
266
app/assets/stylesheets/new_nav.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
150
app/assets/stylesheets/new_sidebar.scss
Normal file
150
app/assets/stylesheets/new_sidebar.scss
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in a new issue