Merge remote-tracking branch 'upstream/master' into 33360-generate-kubeconfig
* upstream/master: (888 commits) Fix Rubocop offense Use a previous approach for cycle analytics dummy pipeline Allow admin to disable all restricted visibility levels Removes file_name_regex from Gitlab::Regex Remove IIFEs around several javascript classes Update CHANGELOG.md for 9.3.5 Add ProjectPathHelper cop Create and use project path helpers that only need a project, no namespace Handles realtime with 2 states for environments table Revert "Merge branch '18000-remember-me-for-oauth-login' into 'master'" Allow creation of files and directories with spaces in web UI Disable Flipper memoizer in tests to avoid transient failures Introduce cache policies for CI jobs fix sidebar padding for full-width items (Time Tracking help) Replace 'snippets/snippets.feature' spinach with rspec Rename ci_config_file to ci_config_path Add back Pipeline#ci_yaml_file_path due to all the troubles Revert change to design. Go back to scrollable page Fix cycle analytics tests by making pipeline valid Fixes the column widths for the new navigation options in settings ...
This commit is contained in:
commit
ef7deb18df
|
@ -11,6 +11,7 @@
|
|||
"gon": false,
|
||||
"localStorage": false
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"filenames",
|
||||
"import",
|
||||
|
|
|
@ -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
|
||||
|
@ -59,3 +60,4 @@ eslint-report.html
|
|||
/.gitlab_workhorse_secret
|
||||
/webpack-report/
|
||||
/locale/**/LC_MESSAGES
|
||||
/.rspec
|
||||
|
|
|
@ -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:
|
||||
|
@ -461,6 +464,7 @@ karma:
|
|||
- coverage-javascript/
|
||||
|
||||
codeclimate:
|
||||
<<: *except-docs
|
||||
before_script: []
|
||||
image: docker:latest
|
||||
stage: test
|
||||
|
@ -470,8 +474,8 @@ codeclimate:
|
|||
services:
|
||||
- docker:dind
|
||||
script:
|
||||
- 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]
|
||||
|
||||
|
@ -546,3 +550,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
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
Please read this!
|
||||
|
||||
Before opening a new issue, make sure to search for keywords in the issues
|
||||
filtered by the "regression" or "bug" label:
|
||||
filtered by the "regression" or "bug" label.
|
||||
|
||||
For the Community Edition issue tracker:
|
||||
|
||||
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=regression
|
||||
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=bug
|
||||
|
||||
For the Enterprise Edition issue tracker:
|
||||
|
||||
- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=regression
|
||||
- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=bug
|
||||
|
||||
and verify the issue you're about to submit isn't a duplicate.
|
||||
|
||||
Please remove this notice if you're confident your issue isn't a duplicate.
|
||||
|
|
|
@ -3,8 +3,14 @@ Please read this!
|
|||
Before opening a new issue, make sure to search for keywords in the issues
|
||||
filtered by the "feature proposal" label:
|
||||
|
||||
For the Community Edition issue tracker:
|
||||
|
||||
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=feature+proposal
|
||||
|
||||
For the Enterprise Edition issue tracker:
|
||||
|
||||
- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=feature+proposal
|
||||
|
||||
and verify the issue you're about to submit isn't a duplicate.
|
||||
|
||||
Please remove this notice if you're confident your issue isn't a duplicate.
|
||||
|
@ -21,12 +27,24 @@ Please remove this notice if you're confident your issue isn't a duplicate.
|
|||
|
||||
### Documentation blurb
|
||||
|
||||
(Write the start of the documentation of this feature here, include:
|
||||
#### Overview
|
||||
|
||||
1. Why should someone use it; what's the underlying problem.
|
||||
2. What is the solution.
|
||||
3. How does someone use this
|
||||
What is it?
|
||||
Why should someone use this feature?
|
||||
What is the underlying (business) problem?
|
||||
How do you use this feature?
|
||||
|
||||
During implementation, this can then be copied and used as a starter for the documentation.)
|
||||
#### Use cases
|
||||
|
||||
/label ~"feature proposal"
|
||||
Who is this for? Provide one or more use cases.
|
||||
|
||||
### Feature checklist
|
||||
|
||||
Make sure these are completed before closing the issue,
|
||||
with a link to the relevant commit.
|
||||
|
||||
- [ ] [Feature assurance](https://about.gitlab.com/handbook/product/#feature-assurance)
|
||||
- [ ] Documentation
|
||||
- [ ] Added to [features.yml](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/features.yml)
|
||||
|
||||
/label ~"feature proposal"
|
15
.rubocop.yml
15
.rubocop.yml
|
@ -164,6 +164,11 @@ Style/DefWithParentheses:
|
|||
Style/Documentation:
|
||||
Enabled: false
|
||||
|
||||
# Multi-line method chaining should be done with leading dots.
|
||||
Style/DotPosition:
|
||||
Enabled: true
|
||||
EnforcedStyle: leading
|
||||
|
||||
# This cop checks for uses of double negation (!!) to convert something
|
||||
# to a boolean value. As this is both cryptic and usually redundant, it
|
||||
# should be avoided.
|
||||
|
@ -960,6 +965,10 @@ RSpec/AnyInstance:
|
|||
RSpec/BeEql:
|
||||
Enabled: true
|
||||
|
||||
# We don't enforce this as we use this technique in a few places.
|
||||
RSpec/BeforeAfterAll:
|
||||
Enabled: false
|
||||
|
||||
# Check that the first argument to the top level describe is the tested class or
|
||||
# module.
|
||||
RSpec/DescribeClass:
|
||||
|
@ -1019,6 +1028,12 @@ RSpec/FilePath:
|
|||
RSpec/Focus:
|
||||
Enabled: true
|
||||
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: is_expected, should
|
||||
RSpec/ImplicitExpect:
|
||||
Enabled: true
|
||||
EnforcedStyle: is_expected
|
||||
|
||||
# Checks for the usage of instance variables.
|
||||
RSpec/InstanceVariable:
|
||||
Enabled: false
|
||||
|
|
|
@ -6,10 +6,6 @@
|
|||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
# Offense count: 54
|
||||
RSpec/BeforeAfterAll:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 233
|
||||
RSpec/EmptyLineAfterFinalLet:
|
||||
Enabled: false
|
||||
|
@ -24,12 +20,6 @@ RSpec/EmptyLineAfterSubject:
|
|||
RSpec/HookArgument:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 12
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: is_expected, should
|
||||
RSpec/ImplicitExpect:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 11
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: it_behaves_like, it_should_behave_like
|
||||
|
@ -88,13 +78,6 @@ Security/YAMLLoad:
|
|||
Style/BarePercentLiterals:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 1403
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: leading, trailing
|
||||
Style/DotPosition:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 5
|
||||
# Cop supports --auto-correct.
|
||||
Style/EachWithObject:
|
||||
|
|
253
CHANGELOG.md
253
CHANGELOG.md
|
@ -2,6 +2,259 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 9.3.5 (2017-07-05)
|
||||
|
||||
- Remove "Remove from board" button from backlog and closed list. !12430
|
||||
- Do not delete protected branches when deleting all merged branches. !12624
|
||||
- Set default for Remove source branch to false.
|
||||
- Prevent accidental deletion of protected MR source branch by repeating checks before actual deletion.
|
||||
- Expires full_path cache after a repository is renamed/transferred.
|
||||
|
||||
## 9.3.4 (2017-07-03)
|
||||
|
||||
- No changes.
|
||||
|
||||
## 9.3.3 (2017-06-30)
|
||||
|
||||
- Fix head pipeline stored in merge request for external pipelines. !12478
|
||||
- Bring back branches badge to main project page. !12548
|
||||
- Fix diff of requirements.txt file by not matching newlines as part of package names.
|
||||
- Perform housekeeping only when an import of a fresh project is completed.
|
||||
- Fixed issue boards closed list not showing all closed issues.
|
||||
- Fixed multi-line markdown tooltip buttons in issue edit form.
|
||||
|
||||
## 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)
|
||||
|
||||
## 9.2.6 (2017-06-16)
|
||||
|
||||
- Fix the last coverage in trace log should be extracted. !11128 (dosuken123)
|
||||
|
|
|
@ -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 @@
|
|||
0.11.2
|
||||
0.14.0
|
||||
|
|
|
@ -1 +1 @@
|
|||
5.0.5
|
||||
5.1.1
|
||||
|
|
|
@ -1 +1 @@
|
|||
2.1.1
|
||||
2.2.0
|
||||
|
|
19
Gemfile
19
Gemfile
|
@ -2,7 +2,7 @@ source 'https://rubygems.org'
|
|||
|
||||
gem 'rails', '4.2.8'
|
||||
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
|
||||
gem 'bootsnap', '~> 1.0.0'
|
||||
gem 'bootsnap', '~> 1.1'
|
||||
|
||||
# Responders respond_to and respond_with
|
||||
gem 'responders', '~> 2.0'
|
||||
|
@ -86,7 +86,7 @@ gem 'kaminari', '~> 0.17.0'
|
|||
gem 'hamlit', '~> 2.6.1'
|
||||
|
||||
# Files attachments
|
||||
gem 'carrierwave', '~> 1.0'
|
||||
gem 'carrierwave', '~> 1.1'
|
||||
|
||||
# Drag and Drop UI
|
||||
gem 'dropzonejs-rails', '~> 0.7.1'
|
||||
|
@ -123,6 +123,7 @@ gem 'asciidoctor', '~> 1.5.2'
|
|||
gem 'asciidoctor-plantuml', '0.0.7'
|
||||
gem 'rouge', '~> 2.0'
|
||||
gem 'truncato', '~> 0.7.8'
|
||||
gem 'bootstrap_form', '~> 2.7.0'
|
||||
|
||||
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
|
||||
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
|
||||
|
@ -158,7 +159,7 @@ gem 'rufus-scheduler', '~> 3.4'
|
|||
gem 'httparty', '~> 0.13.3'
|
||||
|
||||
# Colored output to console
|
||||
gem 'rainbow', '~> 2.1.0'
|
||||
gem 'rainbow', '~> 2.2'
|
||||
|
||||
# GitLab settings
|
||||
gem 'settingslogic', '~> 2.0.9'
|
||||
|
@ -254,12 +255,13 @@ gem 'net-ssh', '~> 3.0.1'
|
|||
gem 'base32', '~> 0.3.0'
|
||||
|
||||
# Sentry integration
|
||||
gem 'sentry-raven', '~> 2.4.0'
|
||||
gem 'sentry-raven', '~> 2.5.3'
|
||||
|
||||
gem 'premailer-rails', '~> 1.9.0'
|
||||
gem 'premailer-rails', '~> 1.9.7'
|
||||
|
||||
# I18n
|
||||
gem 'ruby_parser', '~> 3.8', require: false
|
||||
gem 'rails-i18n', '~> 4.0.9'
|
||||
gem 'gettext_i18n_rails', '~> 1.8.0'
|
||||
gem 'gettext_i18n_rails_js', '~> 1.2.0'
|
||||
gem 'gettext', '~> 3.2.2', require: false, group: :development
|
||||
|
@ -283,6 +285,7 @@ group :metrics do
|
|||
|
||||
# Prometheus
|
||||
gem 'prometheus-client-mmap', '~>0.7.0.beta5'
|
||||
gem 'raindrops', '~> 0.18'
|
||||
end
|
||||
|
||||
group :development do
|
||||
|
@ -353,7 +356,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'
|
||||
|
@ -373,7 +376,7 @@ gem 'ruby-prof', '~> 0.16.2'
|
|||
gem 'oauth2', '~> 1.4'
|
||||
|
||||
# Soft deletion
|
||||
gem 'paranoia', '~> 2.2'
|
||||
gem 'paranoia', '~> 2.3.1'
|
||||
|
||||
# Health check
|
||||
gem 'health_check', '~> 2.6.0'
|
||||
|
@ -383,7 +386,7 @@ gem 'vmstat', '~> 2.3.0'
|
|||
gem 'sys-filesystem', '~> 1.1.6'
|
||||
|
||||
# Gitaly GRPC client
|
||||
gem 'gitaly', '~> 0.8.0'
|
||||
gem 'gitaly', '~> 0.9.0'
|
||||
|
||||
gem 'toml-rb', '~> 0.3.15', require: false
|
||||
|
||||
|
|
63
Gemfile.lock
63
Gemfile.lock
|
@ -83,11 +83,12 @@ GEM
|
|||
bindata (2.3.5)
|
||||
binding_of_caller (0.7.2)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bootsnap (1.0.0)
|
||||
bootsnap (1.1.1)
|
||||
msgpack (~> 1.0)
|
||||
bootstrap-sass (3.3.6)
|
||||
autoprefixer-rails (>= 5.2.1)
|
||||
sass (>= 3.3.4)
|
||||
bootstrap_form (2.7.0)
|
||||
brakeman (3.6.1)
|
||||
browser (2.2.0)
|
||||
builder (3.2.3)
|
||||
|
@ -108,7 +109,7 @@ GEM
|
|||
capybara-screenshot (1.0.14)
|
||||
capybara (>= 1.0, < 3)
|
||||
launchy
|
||||
carrierwave (1.0.0)
|
||||
carrierwave (1.1.0)
|
||||
activemodel (>= 4.0.0)
|
||||
activesupport (>= 4.0.0)
|
||||
mime-types (>= 1.16)
|
||||
|
@ -138,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)
|
||||
|
@ -277,7 +278,7 @@ GEM
|
|||
po_to_json (>= 1.0.0)
|
||||
rails (>= 3.2.0)
|
||||
gherkin-ruby (0.3.2)
|
||||
gitaly (0.8.0)
|
||||
gitaly (0.9.0)
|
||||
google-protobuf (~> 3.1)
|
||||
grpc (~> 1.0)
|
||||
github-linguist (4.7.6)
|
||||
|
@ -353,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)
|
||||
|
@ -367,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)
|
||||
|
@ -462,7 +463,7 @@ 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)
|
||||
|
@ -546,8 +547,8 @@ GEM
|
|||
rubypants (~> 0.2)
|
||||
orm_adapter (0.5.0)
|
||||
os (0.9.6)
|
||||
paranoia (2.2.0)
|
||||
activerecord (>= 4.0, < 5.1)
|
||||
paranoia (2.3.1)
|
||||
activerecord (>= 4.0, < 5.2)
|
||||
parser (2.4.0.0)
|
||||
ast (~> 2.2)
|
||||
path_expander (1.0.1)
|
||||
|
@ -591,14 +592,15 @@ 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)
|
||||
mmap2 (~> 2.2.6)
|
||||
prometheus-client-mmap (0.7.0.beta8)
|
||||
mmap2 (~> 2.2, >= 2.2.7)
|
||||
pry (0.10.4)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.8.1)
|
||||
|
@ -646,13 +648,17 @@ GEM
|
|||
rails-deprecated_sanitizer (>= 1.0.1)
|
||||
rails-html-sanitizer (1.0.3)
|
||||
loofah (~> 2.0)
|
||||
rails-i18n (4.0.9)
|
||||
i18n (~> 0.7)
|
||||
railties (~> 4.0)
|
||||
railties (4.2.8)
|
||||
actionpack (= 4.2.8)
|
||||
activesupport (= 4.2.8)
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
rainbow (2.1.0)
|
||||
raindrops (0.17.0)
|
||||
rainbow (2.2.2)
|
||||
rake
|
||||
raindrops (0.18.0)
|
||||
rake (10.5.0)
|
||||
rblineprof (0.3.6)
|
||||
debugger-ruby_core_source (~> 1.3)
|
||||
|
@ -769,7 +775,7 @@ GEM
|
|||
activesupport (>= 3.1)
|
||||
select2-rails (3.5.9.3)
|
||||
thor (~> 0.14)
|
||||
sentry-raven (2.4.0)
|
||||
sentry-raven (2.5.3)
|
||||
faraday (>= 0.7.6, < 1.0)
|
||||
settingslogic (2.0.9)
|
||||
sexp_processor (4.9.0)
|
||||
|
@ -885,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
|
||||
|
@ -924,15 +930,16 @@ DEPENDENCIES
|
|||
benchmark-ips (~> 2.3.0)
|
||||
better_errors (~> 2.1.0)
|
||||
binding_of_caller (~> 0.7.2)
|
||||
bootsnap (~> 1.0.0)
|
||||
bootsnap (~> 1.1)
|
||||
bootstrap-sass (~> 3.3.0)
|
||||
bootstrap_form (~> 2.7.0)
|
||||
brakeman (~> 3.6.0)
|
||||
browser (~> 2.2)
|
||||
bullet (~> 5.5.0)
|
||||
bundler-audit (~> 0.5.0)
|
||||
capybara (~> 2.6.2)
|
||||
capybara-screenshot (~> 1.0.0)
|
||||
carrierwave (~> 1.0)
|
||||
carrierwave (~> 1.1)
|
||||
charlock_holmes (~> 0.7.3)
|
||||
chronic (~> 0.10.2)
|
||||
chronic_duration (~> 0.10.6)
|
||||
|
@ -973,7 +980,7 @@ DEPENDENCIES
|
|||
gettext (~> 3.2.2)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.2.0)
|
||||
gitaly (~> 0.8.0)
|
||||
gitaly (~> 0.9.0)
|
||||
github-linguist (~> 4.7.0)
|
||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||
gitlab-markup (~> 1.5.1)
|
||||
|
@ -1031,7 +1038,7 @@ DEPENDENCIES
|
|||
omniauth-twitter (~> 1.2.0)
|
||||
omniauth_crowd (~> 2.2.0)
|
||||
org-ruby (~> 0.9.12)
|
||||
paranoia (~> 2.2)
|
||||
paranoia (~> 2.3.1)
|
||||
peek (~> 1.0.1)
|
||||
peek-gc (~> 0.0.2)
|
||||
peek-host (~> 1.0.0)
|
||||
|
@ -1043,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)
|
||||
|
@ -1053,7 +1060,9 @@ DEPENDENCIES
|
|||
rack-proxy (~> 0.6.0)
|
||||
rails (= 4.2.8)
|
||||
rails-deprecated_sanitizer (~> 1.0.3)
|
||||
rainbow (~> 2.1.0)
|
||||
rails-i18n (~> 4.0.9)
|
||||
rainbow (~> 2.2)
|
||||
raindrops (~> 0.18)
|
||||
rblineprof (~> 0.3.6)
|
||||
rdoc (~> 4.2)
|
||||
recaptcha (~> 3.0)
|
||||
|
@ -1081,7 +1090,7 @@ DEPENDENCIES
|
|||
scss_lint (~> 0.47.0)
|
||||
seed-fu (~> 2.3.5)
|
||||
select2-rails (~> 3.5.9)
|
||||
sentry-raven (~> 2.4.0)
|
||||
sentry-raven (~> 2.5.3)
|
||||
settingslogic (~> 2.0.9)
|
||||
sham_rack (~> 1.3.6)
|
||||
shoulda-matchers (~> 2.8.0)
|
||||
|
@ -1114,9 +1123,9 @@ 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)
|
||||
|
||||
BUNDLED WITH
|
||||
1.15.0
|
||||
1.15.1
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
|
@ -1,12 +1,8 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
/* 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';
|
||||
|
||||
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
|
||||
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
|
||||
const requestAnimationFrame = window.requestAnimationFrame ||
|
||||
|
@ -16,8 +12,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',
|
||||
|
@ -29,186 +23,144 @@ 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);
|
||||
class AwardsHandler {
|
||||
constructor(emoji) {
|
||||
this.emoji = emoji;
|
||||
this.eventListeners = [];
|
||||
// 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');
|
||||
if ($menu.length === 0) {
|
||||
requestAnimationFrame(() => {
|
||||
this.createEmojiMenu();
|
||||
});
|
||||
}
|
||||
});
|
||||
this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.showEmojiMenu($(e.currentTarget));
|
||||
});
|
||||
|
||||
this.registerEventListener('on', $('html'), 'click', (e) => {
|
||||
const $target = $(e.target);
|
||||
if (!$target.closest('.emoji-menu-content').length) {
|
||||
$('.js-awards-block.current').removeClass('current');
|
||||
}
|
||||
if (!$target.closest('.emoji-menu').length) {
|
||||
if ($('.emoji-menu').is(':visible')) {
|
||||
$('.js-add-award.is-active').removeClass('is-active');
|
||||
$('.emoji-menu').removeClass('is-visible');
|
||||
}
|
||||
}
|
||||
});
|
||||
this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', (e) => {
|
||||
e.preventDefault();
|
||||
const $target = $(e.currentTarget);
|
||||
const $glEmojiElement = $target.find('gl-emoji');
|
||||
const $spriteIconElement = $target.find('.icon');
|
||||
const emojiName = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
|
||||
|
||||
$target.closest('.js-awards-block').addClass('current');
|
||||
this.addAward(this.getVotesBlock(), this.getAwardUrl(), emojiName);
|
||||
});
|
||||
}
|
||||
|
||||
registerEventListener(method = 'on', element, ...args) {
|
||||
element[method].call(element, ...args);
|
||||
this.eventListeners.push({
|
||||
element,
|
||||
args,
|
||||
});
|
||||
}
|
||||
|
||||
showEmojiMenu($addBtn) {
|
||||
if ($addBtn.hasClass('js-note-emoji')) {
|
||||
$addBtn.closest('.note').find('.js-awards-block').addClass('current');
|
||||
} else {
|
||||
$addBtn.closest('.js-awards-block').addClass('current');
|
||||
}
|
||||
|
||||
return currentCategoryMap;
|
||||
}, {
|
||||
activity: [],
|
||||
people: [],
|
||||
nature: [],
|
||||
food: [],
|
||||
travel: [],
|
||||
objects: [],
|
||||
symbols: [],
|
||||
flags: [],
|
||||
});
|
||||
}
|
||||
|
||||
function renderCategory(name, emojiList, opts = {}) {
|
||||
return `
|
||||
<h5 class="emoji-menu-title">
|
||||
${name}
|
||||
</h5>
|
||||
<ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
|
||||
${emojiList.map(emojiName => `
|
||||
<li class="emoji-menu-list-item">
|
||||
<button class="emoji-menu-btn text-center js-emoji-btn" type="button">
|
||||
${glEmojiTag(emojiName, {
|
||||
sprite: true,
|
||||
})}
|
||||
</button>
|
||||
</li>
|
||||
`).join('\n')}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
|
||||
function AwardsHandler() {
|
||||
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');
|
||||
if ($menu.length === 0) {
|
||||
requestAnimationFrame(() => {
|
||||
this.createEmojiMenu();
|
||||
const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent();
|
||||
const $userAuthored = this.isUserAuthored($addBtn);
|
||||
if ($menu.length) {
|
||||
if ($menu.is('.is-visible')) {
|
||||
$addBtn.removeClass('is-active');
|
||||
$menu.removeClass('is-visible');
|
||||
$('.js-emoji-menu-search').blur();
|
||||
} else {
|
||||
$addBtn.addClass('is-active');
|
||||
this.positionMenu($menu, $addBtn);
|
||||
$menu.addClass('is-visible');
|
||||
$('.js-emoji-menu-search').focus();
|
||||
}
|
||||
} else {
|
||||
$addBtn.addClass('is-loading is-active');
|
||||
this.createEmojiMenu(() => {
|
||||
const $createdMenu = $('.emoji-menu');
|
||||
$addBtn.removeClass('is-loading');
|
||||
this.positionMenu($createdMenu, $addBtn);
|
||||
return setTimeout(() => {
|
||||
$createdMenu.addClass('is-visible');
|
||||
$('.js-emoji-menu-search').focus();
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
// Prebuild the categoryMap
|
||||
categoryMap = categoryMap || buildCategoryMap();
|
||||
});
|
||||
this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.showEmojiMenu($(e.currentTarget));
|
||||
});
|
||||
|
||||
this.registerEventListener('on', $('html'), 'click', (e) => {
|
||||
const $target = $(e.target);
|
||||
if (!$target.closest('.emoji-menu-content').length) {
|
||||
$('.js-awards-block.current').removeClass('current');
|
||||
$thumbsBtn.toggleClass('disabled', $userAuthored);
|
||||
}
|
||||
|
||||
// Create the emoji menu with the first category of emojis.
|
||||
// Then render the remaining categories of emojis one by one to avoid jank.
|
||||
createEmojiMenu(callback) {
|
||||
if (this.isCreatingEmojiMenu) {
|
||||
return;
|
||||
}
|
||||
if (!$target.closest('.emoji-menu').length) {
|
||||
if ($('.emoji-menu').is(':visible')) {
|
||||
$('.js-add-award.is-active').removeClass('is-active');
|
||||
$('.emoji-menu').removeClass('is-visible');
|
||||
}
|
||||
this.isCreatingEmojiMenu = true;
|
||||
|
||||
// Render the first category
|
||||
const categoryMap = this.emoji.getEmojiCategoryMap();
|
||||
const categoryNameKey = Object.keys(categoryMap)[0];
|
||||
const emojisInCategory = categoryMap[categoryNameKey];
|
||||
const firstCategory = this.renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
|
||||
|
||||
// Render the frequently used
|
||||
const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
|
||||
let frequentlyUsedCatgegory = '';
|
||||
if (frequentlyUsedEmojis.length > 0) {
|
||||
frequentlyUsedCatgegory = this.renderCategory('Frequently used', frequentlyUsedEmojis, {
|
||||
menuListClass: 'frequent-emojis',
|
||||
});
|
||||
}
|
||||
});
|
||||
this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', (e) => {
|
||||
e.preventDefault();
|
||||
const $target = $(e.currentTarget);
|
||||
const $glEmojiElement = $target.find('gl-emoji');
|
||||
const $spriteIconElement = $target.find('.icon');
|
||||
const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
|
||||
|
||||
$target.closest('.js-awards-block').addClass('current');
|
||||
this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji);
|
||||
});
|
||||
}
|
||||
const emojiMenuMarkup = `
|
||||
<div class="emoji-menu">
|
||||
<input type="text" name="emoji-menu-search" value="" class="js-emoji-menu-search emoji-search search-input form-control" placeholder="Search emoji" />
|
||||
|
||||
AwardsHandler.prototype.registerEventListener = function registerEventListener(method = 'on', element, ...args) {
|
||||
element[method].call(element, ...args);
|
||||
this.eventListeners.push({
|
||||
element,
|
||||
args,
|
||||
});
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) {
|
||||
if ($addBtn.hasClass('js-note-emoji')) {
|
||||
$addBtn.closest('.note').find('.js-awards-block').addClass('current');
|
||||
} else {
|
||||
$addBtn.closest('.js-awards-block').addClass('current');
|
||||
}
|
||||
|
||||
const $menu = $('.emoji-menu');
|
||||
const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent();
|
||||
const $userAuthored = this.isUserAuthored($addBtn);
|
||||
if ($menu.length) {
|
||||
if ($menu.is('.is-visible')) {
|
||||
$addBtn.removeClass('is-active');
|
||||
$menu.removeClass('is-visible');
|
||||
$('.js-emoji-menu-search').blur();
|
||||
} else {
|
||||
$addBtn.addClass('is-active');
|
||||
this.positionMenu($menu, $addBtn);
|
||||
$menu.addClass('is-visible');
|
||||
$('.js-emoji-menu-search').focus();
|
||||
}
|
||||
} else {
|
||||
$addBtn.addClass('is-loading is-active');
|
||||
this.createEmojiMenu(() => {
|
||||
const $createdMenu = $('.emoji-menu');
|
||||
$addBtn.removeClass('is-loading');
|
||||
this.positionMenu($createdMenu, $addBtn);
|
||||
return setTimeout(() => {
|
||||
$createdMenu.addClass('is-visible');
|
||||
$('.js-emoji-menu-search').focus();
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
$thumbsBtn.toggleClass('disabled', $userAuthored);
|
||||
};
|
||||
|
||||
// Create the emoji menu with the first category of emojis.
|
||||
// Then render the remaining categories of emojis one by one to avoid jank.
|
||||
AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) {
|
||||
if (this.isCreatingEmojiMenu) {
|
||||
return;
|
||||
}
|
||||
this.isCreatingEmojiMenu = true;
|
||||
|
||||
// Render the first category
|
||||
categoryMap = categoryMap || buildCategoryMap();
|
||||
const categoryNameKey = Object.keys(categoryMap)[0];
|
||||
const emojisInCategory = categoryMap[categoryNameKey];
|
||||
const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
|
||||
|
||||
// Render the frequently used
|
||||
const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
|
||||
let frequentlyUsedCatgegory = '';
|
||||
if (frequentlyUsedEmojis.length > 0) {
|
||||
frequentlyUsedCatgegory = renderCategory('Frequently used', frequentlyUsedEmojis, {
|
||||
menuListClass: 'frequent-emojis',
|
||||
});
|
||||
}
|
||||
|
||||
const emojiMenuMarkup = `
|
||||
<div class="emoji-menu">
|
||||
<input type="text" name="emoji-menu-search" value="" class="js-emoji-menu-search emoji-search search-input form-control" placeholder="Search emoji" />
|
||||
|
||||
<div class="emoji-menu-content">
|
||||
${frequentlyUsedCatgegory}
|
||||
${firstCategory}
|
||||
<div class="emoji-menu-content">
|
||||
${frequentlyUsedCatgegory}
|
||||
${firstCategory}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup);
|
||||
document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup);
|
||||
|
||||
this.addRemainingEmojiMenuCategories();
|
||||
this.setupSearch();
|
||||
if (callback) {
|
||||
callback();
|
||||
this.addRemainingEmojiMenuCategories();
|
||||
this.setupSearch();
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AwardsHandler
|
||||
.prototype
|
||||
.addRemainingEmojiMenuCategories = function addRemainingEmojiMenuCategories() {
|
||||
addRemainingEmojiMenuCategories() {
|
||||
if (this.isAddingRemainingEmojiMenuCategories) {
|
||||
return;
|
||||
}
|
||||
this.isAddingRemainingEmojiMenuCategories = true;
|
||||
|
||||
categoryMap = categoryMap || buildCategoryMap();
|
||||
const categoryMap = this.emoji.getEmojiCategoryMap();
|
||||
|
||||
// Avoid the jank and render the remaining categories separately
|
||||
// This will take more time, but makes UI more responsive
|
||||
|
@ -220,7 +172,7 @@ AwardsHandler
|
|||
promiseChain.then(() =>
|
||||
new Promise((resolve) => {
|
||||
const emojisInCategory = categoryMap[categoryNameKey];
|
||||
const categoryMarkup = renderCategory(
|
||||
const categoryMarkup = this.renderCategory(
|
||||
categoryLabelMap[categoryNameKey],
|
||||
emojisInCategory,
|
||||
);
|
||||
|
@ -243,179 +195,186 @@ AwardsHandler
|
|||
emojiContentElement.insertAdjacentHTML('beforeend', '<p>We encountered an error while adding the remaining categories</p>');
|
||||
throw new Error(`Error occurred in addRemainingEmojiMenuCategories: ${err.message}`);
|
||||
});
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.positionMenu = function positionMenu($menu, $addBtn) {
|
||||
const position = $addBtn.data('position');
|
||||
// The menu could potentially be off-screen or in a hidden overflow element
|
||||
// So we position the element absolute in the body
|
||||
const css = {
|
||||
top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`,
|
||||
};
|
||||
if (position === 'right') {
|
||||
css.left = `${($addBtn.offset().left - $menu.outerWidth()) + 20}px`;
|
||||
$menu.addClass('is-aligned-right');
|
||||
} else {
|
||||
css.left = `${$addBtn.offset().left}px`;
|
||||
$menu.removeClass('is-aligned-right');
|
||||
}
|
||||
return $menu.css(css);
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.addAward = function addAward(
|
||||
votesBlock,
|
||||
awardUrl,
|
||||
emoji,
|
||||
checkMutuality,
|
||||
callback,
|
||||
) {
|
||||
const normalizedEmoji = this.normalizeEmojiName(emoji);
|
||||
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
|
||||
this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
|
||||
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
|
||||
return typeof callback === 'function' ? callback() : undefined;
|
||||
});
|
||||
$('.emoji-menu').removeClass('is-visible');
|
||||
$('.js-add-award.is-active').removeClass('is-active');
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar(
|
||||
votesBlock,
|
||||
emoji,
|
||||
checkForMutuality,
|
||||
) {
|
||||
if (checkForMutuality || checkForMutuality === null) {
|
||||
this.checkMutuality(votesBlock, emoji);
|
||||
renderCategory(name, emojiList, opts = {}) {
|
||||
return `
|
||||
<h5 class="emoji-menu-title">
|
||||
${name}
|
||||
</h5>
|
||||
<ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
|
||||
${emojiList.map(emojiName => `
|
||||
<li class="emoji-menu-list-item">
|
||||
<button class="emoji-menu-btn text-center js-emoji-btn" type="button">
|
||||
${this.emoji.glEmojiTag(emojiName, {
|
||||
sprite: true,
|
||||
})}
|
||||
</button>
|
||||
</li>
|
||||
`).join('\n')}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
this.addEmojiToFrequentlyUsedList(emoji);
|
||||
const normalizedEmoji = this.normalizeEmojiName(emoji);
|
||||
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
|
||||
if ($emojiButton.length > 0) {
|
||||
if (this.isActive($emojiButton)) {
|
||||
this.decrementCounter($emojiButton, normalizedEmoji);
|
||||
|
||||
positionMenu($menu, $addBtn) {
|
||||
const position = $addBtn.data('position');
|
||||
// The menu could potentially be off-screen or in a hidden overflow element
|
||||
// So we position the element absolute in the body
|
||||
const css = {
|
||||
top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`,
|
||||
};
|
||||
if (position === 'right') {
|
||||
css.left = `${($addBtn.offset().left - $menu.outerWidth()) + 20}px`;
|
||||
$menu.addClass('is-aligned-right');
|
||||
} else {
|
||||
const counter = $emojiButton.find('.js-counter');
|
||||
counter.text(parseInt(counter.text(), 10) + 1);
|
||||
$emojiButton.addClass('active');
|
||||
this.addYouToUserList(votesBlock, normalizedEmoji);
|
||||
this.animateEmoji($emojiButton);
|
||||
css.left = `${$addBtn.offset().left}px`;
|
||||
$menu.removeClass('is-aligned-right');
|
||||
}
|
||||
} else {
|
||||
votesBlock.removeClass('hidden');
|
||||
this.createEmoji(votesBlock, normalizedEmoji);
|
||||
}
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.getVotesBlock = function getVotesBlock() {
|
||||
const currentBlock = $('.js-awards-block.current');
|
||||
let resultantVotesBlock = currentBlock;
|
||||
if (currentBlock.length === 0) {
|
||||
resultantVotesBlock = $('.js-awards-block').eq(0);
|
||||
return $menu.css(css);
|
||||
}
|
||||
|
||||
return resultantVotesBlock;
|
||||
};
|
||||
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
|
||||
const normalizedEmoji = this.emoji.normalizeEmojiName(emoji);
|
||||
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
|
||||
this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
|
||||
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
|
||||
return typeof callback === 'function' ? callback() : undefined;
|
||||
});
|
||||
$('.emoji-menu').removeClass('is-visible');
|
||||
$('.js-add-award.is-active').removeClass('is-active');
|
||||
}
|
||||
|
||||
AwardsHandler.prototype.getAwardUrl = function getAwardUrl() {
|
||||
return this.getVotesBlock().data('award-url');
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.checkMutuality = function checkMutuality(votesBlock, emoji) {
|
||||
const awardUrl = this.getAwardUrl();
|
||||
if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
|
||||
const mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup';
|
||||
const $emojiButton = votesBlock.find(`[data-name="${mutualVote}"]`).parent();
|
||||
const isAlreadyVoted = $emojiButton.hasClass('active');
|
||||
if (isAlreadyVoted) {
|
||||
this.addAward(votesBlock, awardUrl, mutualVote, false);
|
||||
addAwardToEmojiBar(votesBlock, emoji, checkForMutuality) {
|
||||
if (checkForMutuality || checkForMutuality === null) {
|
||||
this.checkMutuality(votesBlock, emoji);
|
||||
}
|
||||
this.addEmojiToFrequentlyUsedList(emoji);
|
||||
const normalizedEmoji = this.emoji.normalizeEmojiName(emoji);
|
||||
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
|
||||
if ($emojiButton.length > 0) {
|
||||
if (this.isActive($emojiButton)) {
|
||||
this.decrementCounter($emojiButton, normalizedEmoji);
|
||||
} else {
|
||||
const counter = $emojiButton.find('.js-counter');
|
||||
counter.text(parseInt(counter.text(), 10) + 1);
|
||||
$emojiButton.addClass('active');
|
||||
this.addYouToUserList(votesBlock, normalizedEmoji);
|
||||
this.animateEmoji($emojiButton);
|
||||
}
|
||||
} else {
|
||||
votesBlock.removeClass('hidden');
|
||||
this.createEmoji(votesBlock, normalizedEmoji);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.isActive = function isActive($emojiButton) {
|
||||
return $emojiButton.hasClass('active');
|
||||
};
|
||||
getVotesBlock() {
|
||||
const currentBlock = $('.js-awards-block.current');
|
||||
let resultantVotesBlock = currentBlock;
|
||||
if (currentBlock.length === 0) {
|
||||
resultantVotesBlock = $('.js-awards-block').eq(0);
|
||||
}
|
||||
|
||||
AwardsHandler.prototype.isUserAuthored = function isUserAuthored($button) {
|
||||
return $button.hasClass('js-user-authored');
|
||||
};
|
||||
return resultantVotesBlock;
|
||||
}
|
||||
|
||||
AwardsHandler.prototype.decrementCounter = function decrementCounter($emojiButton, emoji) {
|
||||
const counter = $('.js-counter', $emojiButton);
|
||||
const counterNumber = parseInt(counter.text(), 10);
|
||||
if (counterNumber > 1) {
|
||||
counter.text(counterNumber - 1);
|
||||
this.removeYouFromUserList($emojiButton);
|
||||
} else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
|
||||
$emojiButton.tooltip('destroy');
|
||||
counter.text('0');
|
||||
this.removeYouFromUserList($emojiButton);
|
||||
if ($emojiButton.parents('.note').length) {
|
||||
getAwardUrl() {
|
||||
return this.getVotesBlock().data('award-url');
|
||||
}
|
||||
|
||||
checkMutuality(votesBlock, emoji) {
|
||||
const awardUrl = this.getAwardUrl();
|
||||
if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
|
||||
const mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup';
|
||||
const $emojiButton = votesBlock.find(`[data-name="${mutualVote}"]`).parent();
|
||||
const isAlreadyVoted = $emojiButton.hasClass('active');
|
||||
if (isAlreadyVoted) {
|
||||
this.addAward(votesBlock, awardUrl, mutualVote, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isActive($emojiButton) {
|
||||
return $emojiButton.hasClass('active');
|
||||
}
|
||||
|
||||
isUserAuthored($button) {
|
||||
return $button.hasClass('js-user-authored');
|
||||
}
|
||||
|
||||
decrementCounter($emojiButton, emoji) {
|
||||
const counter = $('.js-counter', $emojiButton);
|
||||
const counterNumber = parseInt(counter.text(), 10);
|
||||
if (counterNumber > 1) {
|
||||
counter.text(counterNumber - 1);
|
||||
this.removeYouFromUserList($emojiButton);
|
||||
} else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
|
||||
$emojiButton.tooltip('destroy');
|
||||
counter.text('0');
|
||||
this.removeYouFromUserList($emojiButton);
|
||||
if ($emojiButton.parents('.note').length) {
|
||||
this.removeEmoji($emojiButton);
|
||||
}
|
||||
} else {
|
||||
this.removeEmoji($emojiButton);
|
||||
}
|
||||
} else {
|
||||
this.removeEmoji($emojiButton);
|
||||
}
|
||||
return $emojiButton.removeClass('active');
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.removeEmoji = function removeEmoji($emojiButton) {
|
||||
$emojiButton.tooltip('destroy');
|
||||
$emojiButton.remove();
|
||||
const $votesBlock = this.getVotesBlock();
|
||||
if ($votesBlock.find('.js-emoji-btn').length === 0) {
|
||||
$votesBlock.addClass('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.getAwardTooltip = function getAwardTooltip($awardBlock) {
|
||||
return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || '';
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.toSentence = function toSentence(list) {
|
||||
let sentence;
|
||||
if (list.length <= 2) {
|
||||
sentence = list.join(' and ');
|
||||
} else {
|
||||
sentence = `${list.slice(0, -1).join(', ')}, and ${list[list.length - 1]}`;
|
||||
return $emojiButton.removeClass('active');
|
||||
}
|
||||
|
||||
return sentence;
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.removeYouFromUserList = function removeYouFromUserList($emojiButton) {
|
||||
const awardBlock = $emojiButton;
|
||||
const originalTitle = this.getAwardTooltip(awardBlock);
|
||||
const authors = originalTitle.split(FROM_SENTENCE_REGEX);
|
||||
authors.splice(authors.indexOf('You'), 1);
|
||||
return awardBlock
|
||||
.closest('.js-emoji-btn')
|
||||
.removeData('title')
|
||||
.removeAttr('data-title')
|
||||
.removeAttr('data-original-title')
|
||||
.attr('title', this.toSentence(authors))
|
||||
.tooltip('fixTitle');
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.addYouToUserList = function addYouToUserList(votesBlock, emoji) {
|
||||
const awardBlock = this.findEmojiIcon(votesBlock, emoji).parent();
|
||||
const origTitle = this.getAwardTooltip(awardBlock);
|
||||
let users = [];
|
||||
if (origTitle) {
|
||||
users = origTitle.trim().split(FROM_SENTENCE_REGEX);
|
||||
removeEmoji($emojiButton) {
|
||||
$emojiButton.tooltip('destroy');
|
||||
$emojiButton.remove();
|
||||
const $votesBlock = this.getVotesBlock();
|
||||
if ($votesBlock.find('.js-emoji-btn').length === 0) {
|
||||
$votesBlock.addClass('hidden');
|
||||
}
|
||||
}
|
||||
users.unshift('You');
|
||||
return awardBlock
|
||||
.attr('title', this.toSentence(users))
|
||||
.tooltip('fixTitle');
|
||||
};
|
||||
|
||||
AwardsHandler
|
||||
.prototype
|
||||
.createAwardButtonForVotesBlock = function createAwardButtonForVotesBlock(votesBlock, emojiName) {
|
||||
getAwardTooltip($awardBlock) {
|
||||
return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || '';
|
||||
}
|
||||
|
||||
toSentence(list) {
|
||||
let sentence;
|
||||
if (list.length <= 2) {
|
||||
sentence = list.join(' and ');
|
||||
} else {
|
||||
sentence = `${list.slice(0, -1).join(', ')}, and ${list[list.length - 1]}`;
|
||||
}
|
||||
|
||||
return sentence;
|
||||
}
|
||||
|
||||
removeYouFromUserList($emojiButton) {
|
||||
const awardBlock = $emojiButton;
|
||||
const originalTitle = this.getAwardTooltip(awardBlock);
|
||||
const authors = originalTitle.split(FROM_SENTENCE_REGEX);
|
||||
authors.splice(authors.indexOf('You'), 1);
|
||||
return awardBlock
|
||||
.closest('.js-emoji-btn')
|
||||
.removeData('title')
|
||||
.removeAttr('data-title')
|
||||
.removeAttr('data-original-title')
|
||||
.attr('title', this.toSentence(authors))
|
||||
.tooltip('fixTitle');
|
||||
}
|
||||
|
||||
addYouToUserList(votesBlock, emoji) {
|
||||
const awardBlock = this.findEmojiIcon(votesBlock, emoji).parent();
|
||||
const origTitle = this.getAwardTooltip(awardBlock);
|
||||
let users = [];
|
||||
if (origTitle) {
|
||||
users = origTitle.trim().split(FROM_SENTENCE_REGEX);
|
||||
}
|
||||
users.unshift('You');
|
||||
return awardBlock
|
||||
.attr('title', this.toSentence(users))
|
||||
.tooltip('fixTitle');
|
||||
}
|
||||
|
||||
createAwardButtonForVotesBlock(votesBlock, emojiName) {
|
||||
const buttonHtml = `
|
||||
<button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom">
|
||||
${glEmojiTag(emojiName)}
|
||||
${this.emoji.glEmojiTag(emojiName)}
|
||||
<span class="award-control-text js-counter">1</span>
|
||||
</button>
|
||||
`;
|
||||
|
@ -424,144 +383,136 @@ AwardsHandler
|
|||
this.animateEmoji($emojiButton);
|
||||
$('.award-control').tooltip();
|
||||
votesBlock.removeClass('current');
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.animateEmoji = function animateEmoji($emoji) {
|
||||
const className = 'pulse animated once short';
|
||||
$emoji.addClass(className);
|
||||
|
||||
this.registerEventListener('on', $emoji, animationEndEventString, (e) => {
|
||||
$(e.currentTarget).removeClass(className);
|
||||
});
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.createEmoji = function createEmoji(votesBlock, emoji) {
|
||||
if ($('.emoji-menu').length) {
|
||||
this.createAwardButtonForVotesBlock(votesBlock, emoji);
|
||||
}
|
||||
this.createEmojiMenu(() => {
|
||||
this.createAwardButtonForVotesBlock(votesBlock, emoji);
|
||||
});
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.postEmoji = function postEmoji($emojiButton, awardUrl, emoji, callback) {
|
||||
if (this.isUserAuthored($emojiButton)) {
|
||||
this.userAuthored($emojiButton);
|
||||
} else {
|
||||
$.post(awardUrl, {
|
||||
name: emoji,
|
||||
}, (data) => {
|
||||
if (data.ok) {
|
||||
callback();
|
||||
}
|
||||
}).fail(() => new Flash('Something went wrong on our end.'));
|
||||
animateEmoji($emoji) {
|
||||
const className = 'pulse animated once short';
|
||||
$emoji.addClass(className);
|
||||
|
||||
this.registerEventListener('on', $emoji, animationEndEventString, (e) => {
|
||||
$(e.currentTarget).removeClass(className);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.findEmojiIcon = function findEmojiIcon(votesBlock, emoji) {
|
||||
return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`);
|
||||
};
|
||||
createEmoji(votesBlock, emoji) {
|
||||
if ($('.emoji-menu').length) {
|
||||
this.createAwardButtonForVotesBlock(votesBlock, emoji);
|
||||
}
|
||||
this.createEmojiMenu(() => {
|
||||
this.createAwardButtonForVotesBlock(votesBlock, emoji);
|
||||
});
|
||||
}
|
||||
|
||||
AwardsHandler.prototype.userAuthored = function userAuthored($emojiButton) {
|
||||
const oldTitle = this.getAwardTooltip($emojiButton);
|
||||
const newTitle = 'You cannot vote on your own issue, MR and note';
|
||||
gl.utils.updateTooltipTitle($emojiButton, newTitle).tooltip('show');
|
||||
// Restore tooltip back to award list
|
||||
return setTimeout(() => {
|
||||
$emojiButton.tooltip('hide');
|
||||
gl.utils.updateTooltipTitle($emojiButton, oldTitle);
|
||||
}, 2800);
|
||||
};
|
||||
postEmoji($emojiButton, awardUrl, emoji, callback) {
|
||||
if (this.isUserAuthored($emojiButton)) {
|
||||
this.userAuthored($emojiButton);
|
||||
} else {
|
||||
$.post(awardUrl, {
|
||||
name: emoji,
|
||||
}, (data) => {
|
||||
if (data.ok) {
|
||||
callback();
|
||||
}
|
||||
}).fail(() => new Flash('Something went wrong on our end.'));
|
||||
}
|
||||
}
|
||||
|
||||
AwardsHandler.prototype.scrollToAwards = function scrollToAwards() {
|
||||
const options = {
|
||||
scrollTop: $('.awards').offset().top - 110,
|
||||
};
|
||||
return $('body, html').animate(options, 200);
|
||||
};
|
||||
findEmojiIcon(votesBlock, emoji) {
|
||||
return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`);
|
||||
}
|
||||
|
||||
AwardsHandler.prototype.normalizeEmojiName = function normalizeEmojiName(emoji) {
|
||||
return Object.prototype.hasOwnProperty.call(this.aliases, emoji) ? this.aliases[emoji] : emoji;
|
||||
};
|
||||
userAuthored($emojiButton) {
|
||||
const oldTitle = this.getAwardTooltip($emojiButton);
|
||||
const newTitle = 'You cannot vote on your own issue, MR and note';
|
||||
gl.utils.updateTooltipTitle($emojiButton, newTitle).tooltip('show');
|
||||
// Restore tooltip back to award list
|
||||
return setTimeout(() => {
|
||||
$emojiButton.tooltip('hide');
|
||||
gl.utils.updateTooltipTitle($emojiButton, oldTitle);
|
||||
}, 2800);
|
||||
}
|
||||
|
||||
AwardsHandler
|
||||
.prototype
|
||||
.addEmojiToFrequentlyUsedList = function addEmojiToFrequentlyUsedList(emoji) {
|
||||
if (isEmojiNameValid(emoji)) {
|
||||
scrollToAwards() {
|
||||
const options = {
|
||||
scrollTop: $('.awards').offset().top - 110,
|
||||
};
|
||||
return $('body, html').animate(options, 200);
|
||||
}
|
||||
|
||||
addEmojiToFrequentlyUsedList(emoji) {
|
||||
if (this.emoji.isEmojiNameValid(emoji)) {
|
||||
this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji));
|
||||
Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 });
|
||||
}
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmojis() {
|
||||
return this.frequentlyUsedEmojis || (() => {
|
||||
const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
|
||||
this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
|
||||
inputName => isEmojiNameValid(inputName),
|
||||
);
|
||||
|
||||
return this.frequentlyUsedEmojis;
|
||||
})();
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.setupSearch = function setupSearch() {
|
||||
const $search = $('.js-emoji-menu-search');
|
||||
|
||||
this.registerEventListener('on', $search, 'input', (e) => {
|
||||
const term = $(e.target).val().trim();
|
||||
this.searchEmojis(term);
|
||||
});
|
||||
|
||||
const $menu = $('.emoji-menu');
|
||||
this.registerEventListener('on', $menu, transitionEndEventString, (e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
// Clear the search
|
||||
this.searchEmojis('');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.searchEmojis = function searchEmojis(term) {
|
||||
const $search = $('.js-emoji-menu-search');
|
||||
$search.val(term);
|
||||
|
||||
// Clean previous search results
|
||||
$('ul.emoji-menu-search, h5.emoji-search-title').remove();
|
||||
if (term.length > 0) {
|
||||
// Generate a search result block
|
||||
const h5 = $('<h5 class="emoji-search-title"/>').text('Search results');
|
||||
const foundEmojis = this.findMatchingEmojiElements(term).show();
|
||||
const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
|
||||
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
|
||||
$('.emoji-menu-content').append(h5).append(ul);
|
||||
} else {
|
||||
$('.emoji-menu-content').children().show();
|
||||
}
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.findMatchingEmojiElements = function findMatchingEmojiElements(term) {
|
||||
const safeTerm = term.toLowerCase();
|
||||
getFrequentlyUsedEmojis() {
|
||||
return this.frequentlyUsedEmojis || (() => {
|
||||
const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
|
||||
this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
|
||||
inputName => this.emoji.isEmojiNameValid(inputName),
|
||||
);
|
||||
|
||||
const namesMatchingAlias = [];
|
||||
Object.keys(emojiAliases).forEach((alias) => {
|
||||
if (alias.indexOf(safeTerm) >= 0) {
|
||||
namesMatchingAlias.push(emojiAliases[alias]);
|
||||
return this.frequentlyUsedEmojis;
|
||||
})();
|
||||
}
|
||||
|
||||
setupSearch() {
|
||||
const $search = $('.js-emoji-menu-search');
|
||||
|
||||
this.registerEventListener('on', $search, 'input', (e) => {
|
||||
const term = $(e.target).val().trim();
|
||||
this.searchEmojis(term);
|
||||
});
|
||||
|
||||
const $menu = $('.emoji-menu');
|
||||
this.registerEventListener('on', $menu, transitionEndEventString, (e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
// Clear the search
|
||||
this.searchEmojis('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
searchEmojis(term) {
|
||||
const $search = $('.js-emoji-menu-search');
|
||||
$search.val(term);
|
||||
|
||||
// Clean previous search results
|
||||
$('ul.emoji-menu-search, h5.emoji-search-title').remove();
|
||||
if (term.length > 0) {
|
||||
// Generate a search result block
|
||||
const h5 = $('<h5 class="emoji-search-title"/>').text('Search results');
|
||||
const foundEmojis = this.findMatchingEmojiElements(term).show();
|
||||
const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
|
||||
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
|
||||
$('.emoji-menu-content').append(h5).append(ul);
|
||||
} else {
|
||||
$('.emoji-menu-content').children().show();
|
||||
}
|
||||
});
|
||||
const $matchingElements = namesMatchingAlias.concat(safeTerm)
|
||||
.reduce(
|
||||
($result, searchTerm) =>
|
||||
$result.add($(`.emoji-menu-list:not(.frequent-emojis) [data-name*="${searchTerm}"]`)),
|
||||
$([]),
|
||||
);
|
||||
return $matchingElements.closest('li').clone();
|
||||
};
|
||||
}
|
||||
|
||||
AwardsHandler.prototype.destroy = function destroy() {
|
||||
this.eventListeners.forEach((entry) => {
|
||||
entry.element.off.call(entry.element, ...entry.args);
|
||||
});
|
||||
$('.emoji-menu').remove();
|
||||
};
|
||||
findMatchingEmojiElements(query) {
|
||||
const emojiMatches = this.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();
|
||||
}
|
||||
|
||||
export default AwardsHandler;
|
||||
destroy() {
|
||||
this.eventListeners.forEach((entry) => {
|
||||
entry.element.off.call(entry.element, ...entry.args);
|
||||
});
|
||||
$('.emoji-menu').remove();
|
||||
}
|
||||
}
|
||||
|
||||
let awardsHandlerPromise = null;
|
||||
export default function loadAwardsHandler(reload = false) {
|
||||
if (!awardsHandlerPromise || reload) {
|
||||
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji')
|
||||
.then(Emoji => new AwardsHandler(Emoji));
|
||||
}
|
||||
return awardsHandlerPromise;
|
||||
}
|
||||
|
|
|
@ -1,23 +1,8 @@
|
|||
import autosize from 'vendor/autosize';
|
||||
|
||||
$(() => {
|
||||
const $fields = $('.js-autosize');
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const autosizeEls = document.querySelectorAll('.js-autosize');
|
||||
|
||||
$fields.on('autosize:resized', function resized() {
|
||||
const $field = $(this);
|
||||
$field.data('height', $field.outerHeight());
|
||||
});
|
||||
|
||||
$fields.on('resize.autosize', function resize() {
|
||||
const $field = $(this);
|
||||
if ($field.data('height') !== $field.outerHeight()) {
|
||||
$field.data('height', $field.outerHeight());
|
||||
autosize.destroy($field);
|
||||
$field.css('max-height', window.outerHeight);
|
||||
}
|
||||
});
|
||||
|
||||
autosize($fields);
|
||||
autosize.update($fields);
|
||||
$fields.css('resize', 'vertical');
|
||||
autosize(autosizeEls);
|
||||
autosize.update(autosizeEls);
|
||||
});
|
||||
|
|
|
@ -1,75 +1,9 @@
|
|||
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 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,18 +24,26 @@ function installGlEmojiElement() {
|
|||
if (
|
||||
emojiUnicode &&
|
||||
isEmojiUnicode &&
|
||||
!isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion)
|
||||
!isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
|
||||
) {
|
||||
// CSS sprite fallback takes precedence over image fallback
|
||||
if (hasCssSpriteFalback) {
|
||||
// IE 11 doesn't like adding multiple at once :(
|
||||
this.classList.add('emoji-icon');
|
||||
this.classList.add(fallbackSpriteClass);
|
||||
} else if (hasImageFallback) {
|
||||
this.innerHTML = emojiImageTag(name, fallbackSrc);
|
||||
} else {
|
||||
const src = assembleFallbackImageSrc(name);
|
||||
this.innerHTML = emojiImageTag(name, src);
|
||||
import(/* webpackChunkName: 'emoji' */ '../emoji')
|
||||
.then(({ emojiImageTag, emojiFallbackImageSrc }) => {
|
||||
if (hasImageFallback) {
|
||||
this.innerHTML = emojiImageTag(name, fallbackSrc);
|
||||
} else {
|
||||
const src = emojiFallbackImageSrc(name);
|
||||
this.innerHTML = emojiImageTag(name, src);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// do nothing
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -110,9 +52,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]);
|
||||
|
|
|
@ -17,7 +17,7 @@ export default {
|
|||
methods: {
|
||||
submit(e) {
|
||||
e.preventDefault();
|
||||
if (this.title.trim() === '') return;
|
||||
if (this.title.trim() === '') return Promise.resolve();
|
||||
|
||||
this.error = false;
|
||||
|
||||
|
@ -29,7 +29,10 @@ export default {
|
|||
assignees: [],
|
||||
});
|
||||
|
||||
this.list.newIssue(issue)
|
||||
eventHub.$emit(`scroll-board-list-${this.list.id}`);
|
||||
this.cancel();
|
||||
|
||||
return this.list.newIssue(issue)
|
||||
.then(() => {
|
||||
// Need this because our jQuery very kindly disables buttons on ALL form submissions
|
||||
$(this.$refs.submitButton).enable();
|
||||
|
@ -47,9 +50,6 @@ export default {
|
|||
// Show error message
|
||||
this.error = true;
|
||||
});
|
||||
|
||||
eventHub.$emit(`scroll-board-list-${this.list.id}`);
|
||||
this.cancel();
|
||||
},
|
||||
cancel() {
|
||||
this.title = '';
|
||||
|
|
|
@ -34,7 +34,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({
|
|||
},
|
||||
milestoneTitle() {
|
||||
return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
|
||||
}
|
||||
},
|
||||
canRemove() {
|
||||
return !this.list.preset;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
detail: {
|
||||
|
|
|
@ -46,8 +46,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
|
|||
},
|
||||
template: `
|
||||
<div
|
||||
class="block list"
|
||||
v-if="list.type !== 'closed'">
|
||||
class="block list">
|
||||
<button
|
||||
class="btn btn-default btn-block"
|
||||
type="button"
|
||||
|
|
|
@ -11,7 +11,6 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
|
|||
// Issue boards is slightly different, we handle all the requests async
|
||||
// instead or reloading the page, we just re-fire the list ajax requests
|
||||
this.isHandledAsync = true;
|
||||
|
||||
this.cantEdit = cantEdit;
|
||||
}
|
||||
|
||||
|
|
|
@ -112,8 +112,7 @@ class List {
|
|||
.then((resp) => {
|
||||
const data = resp.json();
|
||||
issue.id = data.iid;
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
if (this.issuesSize > 1) {
|
||||
const moveBeforeIid = this.issues[1].id;
|
||||
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid);
|
||||
|
|
|
@ -13,25 +13,21 @@ window.Build = (function () {
|
|||
this.options = options || $('.js-build-options').data();
|
||||
|
||||
this.pageUrl = this.options.pageUrl;
|
||||
this.buildUrl = this.options.buildUrl;
|
||||
this.buildStatus = this.options.buildStatus;
|
||||
this.state = this.options.logState;
|
||||
this.buildStage = this.options.buildStage;
|
||||
this.$document = $(document);
|
||||
this.logBytes = 0;
|
||||
this.scrollOffsetPadding = 30;
|
||||
this.hasBeenScrolled = false;
|
||||
|
||||
this.updateDropdown = this.updateDropdown.bind(this);
|
||||
this.getBuildTrace = this.getBuildTrace.bind(this);
|
||||
this.scrollToBottom = this.scrollToBottom.bind(this);
|
||||
|
||||
this.$body = $('body');
|
||||
this.$buildTrace = $('#build-trace');
|
||||
this.$buildRefreshAnimation = $('.js-build-refresh');
|
||||
this.$truncatedInfo = $('.js-truncated-info');
|
||||
this.$buildTraceOutput = $('.js-build-output');
|
||||
this.$scrollContainer = $('.js-scroll-container');
|
||||
this.$topBar = $('.js-top-bar');
|
||||
|
||||
// Scroll controllers
|
||||
this.$scrollTopBtn = $('.js-scroll-up');
|
||||
|
@ -63,13 +59,22 @@ window.Build = (function () {
|
|||
.off('click')
|
||||
.on('click', this.scrollToBottom.bind(this));
|
||||
|
||||
const scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
|
||||
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
|
||||
|
||||
this.$scrollContainer
|
||||
$(window)
|
||||
.off('scroll')
|
||||
.on('scroll', () => {
|
||||
this.hasBeenScrolled = true;
|
||||
scrollThrottled();
|
||||
const contentHeight = this.$buildTraceOutput.prop('scrollHeight');
|
||||
if (contentHeight > this.windowSize) {
|
||||
// means the user did not scroll, the content was updated.
|
||||
this.windowSize = contentHeight;
|
||||
} else {
|
||||
// User scrolled
|
||||
this.hasBeenScrolled = true;
|
||||
this.toggleScrollAnimation(false);
|
||||
}
|
||||
|
||||
this.scrollThrottled();
|
||||
});
|
||||
|
||||
$(window)
|
||||
|
@ -77,60 +82,73 @@ window.Build = (function () {
|
|||
.on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
|
||||
|
||||
this.updateArtifactRemoveDate();
|
||||
this.initAffixTopArea();
|
||||
|
||||
// eslint-disable-next-line
|
||||
this.getBuildTrace()
|
||||
.then(() => this.toggleScroll())
|
||||
.then(() => {
|
||||
if (!this.hasBeenScrolled) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
});
|
||||
|
||||
this.verifyTopPosition();
|
||||
this.getBuildTrace();
|
||||
}
|
||||
|
||||
Build.prototype.canScroll = function () {
|
||||
return (this.$scrollContainer.prop('scrollHeight') - this.scrollOffsetPadding) > this.$scrollContainer.height();
|
||||
Build.prototype.initAffixTopArea = function () {
|
||||
/**
|
||||
If the browser does not support position sticky, it returns the position as static.
|
||||
If the browser does support sticky, then we allow the browser to handle it, if not
|
||||
then we default back to Bootstraps affix
|
||||
**/
|
||||
if (this.$topBar.css('position') !== 'static') return;
|
||||
|
||||
const offsetTop = this.$buildTrace.offset().top;
|
||||
|
||||
this.$topBar.affix({
|
||||
offset: {
|
||||
top: offsetTop,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Build.prototype.canScroll = function () {
|
||||
return document.body.scrollHeight > window.innerHeight;
|
||||
};
|
||||
|
||||
/**
|
||||
* | | Up | Down |
|
||||
* |--------------------------|----------|----------|
|
||||
* | on scroll bottom | active | disabled |
|
||||
* | on scroll top | disabled | active |
|
||||
* | no scroll | disabled | disabled |
|
||||
* | on.('scroll') is on top | disabled | active |
|
||||
* | on('scroll) is on bottom | active | disabled |
|
||||
*
|
||||
*/
|
||||
Build.prototype.toggleScroll = function () {
|
||||
const currentPosition = this.$scrollContainer.scrollTop();
|
||||
const bottomScroll = currentPosition + this.$scrollContainer.innerHeight();
|
||||
const currentPosition = document.body.scrollTop;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
if (this.canScroll()) {
|
||||
if (currentPosition === 0) {
|
||||
if (currentPosition > 0 &&
|
||||
(document.body.scrollHeight - currentPosition !== windowHeight)) {
|
||||
// User is in the middle of the log
|
||||
|
||||
this.toggleDisableButton(this.$scrollTopBtn, false);
|
||||
this.toggleDisableButton(this.$scrollBottomBtn, false);
|
||||
} else if (currentPosition === 0) {
|
||||
// User is at Top of Build Log
|
||||
|
||||
this.toggleDisableButton(this.$scrollTopBtn, true);
|
||||
this.toggleDisableButton(this.$scrollBottomBtn, false);
|
||||
} else if (bottomScroll === this.$scrollContainer.prop('scrollHeight')) {
|
||||
} else if (document.body.scrollHeight - currentPosition === windowHeight) {
|
||||
// User is at the bottom of the build log.
|
||||
|
||||
this.toggleDisableButton(this.$scrollTopBtn, false);
|
||||
this.toggleDisableButton(this.$scrollBottomBtn, true);
|
||||
} else {
|
||||
this.toggleDisableButton(this.$scrollTopBtn, false);
|
||||
this.toggleDisableButton(this.$scrollBottomBtn, false);
|
||||
}
|
||||
} else {
|
||||
this.toggleDisableButton(this.$scrollTopBtn, true);
|
||||
this.toggleDisableButton(this.$scrollBottomBtn, true);
|
||||
}
|
||||
};
|
||||
|
||||
Build.prototype.scrollToTop = function () {
|
||||
this.hasBeenScrolled = true;
|
||||
this.$scrollContainer.scrollTop(0);
|
||||
this.toggleScroll();
|
||||
Build.prototype.scrollDown = function () {
|
||||
document.body.scrollTop = document.body.scrollHeight;
|
||||
};
|
||||
|
||||
Build.prototype.scrollToBottom = function () {
|
||||
this.scrollDown();
|
||||
this.hasBeenScrolled = true;
|
||||
this.toggleScroll();
|
||||
};
|
||||
|
||||
Build.prototype.scrollToTop = function () {
|
||||
document.body.scrollTop = 0;
|
||||
this.hasBeenScrolled = true;
|
||||
this.$scrollContainer.scrollTop(this.$scrollContainer.prop('scrollHeight'));
|
||||
this.toggleScroll();
|
||||
};
|
||||
|
||||
|
@ -143,47 +161,6 @@ window.Build = (function () {
|
|||
this.$scrollBottomBtn.toggleClass('animate', toggle);
|
||||
};
|
||||
|
||||
/**
|
||||
* Build trace top position depends on the space ocupied by the elments rendered before
|
||||
*/
|
||||
Build.prototype.verifyTopPosition = function () {
|
||||
const $buildPage = $('.build-page');
|
||||
|
||||
const $flashError = $('.alert-wrapper');
|
||||
const $header = $('.build-header', $buildPage);
|
||||
const $runnersStuck = $('.js-build-stuck', $buildPage);
|
||||
const $startsEnvironment = $('.js-environment-container', $buildPage);
|
||||
const $erased = $('.js-build-erased', $buildPage);
|
||||
const prependTopDefault = 20;
|
||||
|
||||
// header + navigation + margin
|
||||
let topPostion = 168;
|
||||
|
||||
if ($header.length) {
|
||||
topPostion += $header.outerHeight();
|
||||
}
|
||||
|
||||
if ($runnersStuck.length) {
|
||||
topPostion += $runnersStuck.outerHeight();
|
||||
}
|
||||
|
||||
if ($startsEnvironment.length) {
|
||||
topPostion += $startsEnvironment.outerHeight() + prependTopDefault;
|
||||
}
|
||||
|
||||
if ($erased.length) {
|
||||
topPostion += $erased.outerHeight() + prependTopDefault;
|
||||
}
|
||||
|
||||
if ($flashError.length) {
|
||||
topPostion += $flashError.outerHeight();
|
||||
}
|
||||
|
||||
this.$buildTrace.css({
|
||||
top: topPostion,
|
||||
});
|
||||
};
|
||||
|
||||
Build.prototype.initSidebar = function () {
|
||||
this.$sidebar = $('.js-build-sidebar');
|
||||
this.$sidebar.niceScroll();
|
||||
|
@ -196,10 +173,13 @@ window.Build = (function () {
|
|||
})
|
||||
.done((log) => {
|
||||
gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
|
||||
|
||||
if (log.state) {
|
||||
this.state = log.state;
|
||||
}
|
||||
|
||||
this.windowSize = this.$buildTraceOutput.prop('scrollHeight');
|
||||
|
||||
if (log.append) {
|
||||
this.$buildTraceOutput.append(log.html);
|
||||
this.logBytes += log.size;
|
||||
|
@ -220,16 +200,14 @@ 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
|
||||
this.getBuildTrace()
|
||||
.then(() => {
|
||||
if (!this.hasBeenScrolled) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
});
|
||||
this.getBuildTrace();
|
||||
}, 4000);
|
||||
} else {
|
||||
this.$buildRefreshAnimation.remove();
|
||||
|
@ -242,7 +220,13 @@ window.Build = (function () {
|
|||
})
|
||||
.fail(() => {
|
||||
this.$buildRefreshAnimation.remove();
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
if (!this.hasBeenScrolled) {
|
||||
this.scrollDown();
|
||||
}
|
||||
})
|
||||
.then(() => this.toggleScroll());
|
||||
};
|
||||
|
||||
Build.prototype.shouldHideSidebarForViewport = function () {
|
||||
|
@ -254,14 +238,11 @@ window.Build = (function () {
|
|||
const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
|
||||
const $toggleButton = $('.js-sidebar-build-toggle-header');
|
||||
|
||||
this.$buildTrace
|
||||
.toggleClass('sidebar-expanded', shouldShow)
|
||||
.toggleClass('sidebar-collapsed', shouldHide);
|
||||
this.$sidebar
|
||||
.toggleClass('right-sidebar-expanded', shouldShow)
|
||||
.toggleClass('right-sidebar-collapsed', shouldHide);
|
||||
|
||||
$('.js-build-page')
|
||||
this.$topBar
|
||||
.toggleClass('sidebar-expanded', shouldShow)
|
||||
.toggleClass('sidebar-collapsed', shouldHide);
|
||||
|
||||
|
@ -274,17 +255,10 @@ window.Build = (function () {
|
|||
|
||||
Build.prototype.sidebarOnResize = function () {
|
||||
this.toggleSidebar(this.shouldHideSidebarForViewport());
|
||||
|
||||
this.verifyTopPosition();
|
||||
|
||||
if (this.canScroll()) {
|
||||
this.toggleScroll();
|
||||
}
|
||||
};
|
||||
|
||||
Build.prototype.sidebarOnClick = function () {
|
||||
if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
|
||||
this.verifyTopPosition();
|
||||
};
|
||||
|
||||
Build.prototype.updateArtifactRemoveDate = function () {
|
||||
|
|
|
@ -1,29 +1,30 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
|
||||
import Vue from 'vue';
|
||||
import VueResource from 'vue-resource';
|
||||
import CommitPipelinesTable from './pipelines_table';
|
||||
|
||||
Vue.use(VueResource);
|
||||
import commitPipelinesTable from './pipelines_table.vue';
|
||||
|
||||
/**
|
||||
* Commits View > Pipelines Tab > Pipelines Table.
|
||||
*
|
||||
* Renders Pipelines table in pipelines tab in the commits show view.
|
||||
* Used in:
|
||||
* - Commit details View > Pipelines Tab > Pipelines Table.
|
||||
* - Merge Request details View > Pipelines Tab > Pipelines Table.
|
||||
* - New Merge Request View > Pipelines Tab > Pipelines Table.
|
||||
*/
|
||||
|
||||
// export for use in merge_request_tabs.js (TODO: remove this hack)
|
||||
const CommitPipelinesTable = Vue.extend(commitPipelinesTable);
|
||||
|
||||
// export for use in merge_request_tabs.js (TODO: remove this hack when we understand how to load
|
||||
// vue.js in merge_request_tabs.js)
|
||||
window.gl = window.gl || {};
|
||||
window.gl.CommitPipelinesTable = CommitPipelinesTable;
|
||||
|
||||
$(() => {
|
||||
gl.commits = gl.commits || {};
|
||||
gl.commits.pipelines = gl.commits.pipelines || {};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
|
||||
|
||||
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
|
||||
gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable().$mount();
|
||||
pipelineTableViewEl.appendChild(gl.commits.pipelines.PipelinesTableBundle.$el);
|
||||
const table = new CommitPipelinesTable({
|
||||
propsData: {
|
||||
endpoint: pipelineTableViewEl.dataset.endpoint,
|
||||
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
|
||||
},
|
||||
}).$mount();
|
||||
pipelineTableViewEl.appendChild(table.$el);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,191 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import Visibility from 'visibilityjs';
|
||||
import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue';
|
||||
import PipelinesService from '../../pipelines/services/pipelines_service';
|
||||
import PipelineStore from '../../pipelines/stores/pipelines_store';
|
||||
import eventHub from '../../pipelines/event_hub';
|
||||
import emptyState from '../../pipelines/components/empty_state.vue';
|
||||
import errorState from '../../pipelines/components/error_state.vue';
|
||||
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import '../../lib/utils/common_utils';
|
||||
import '../../vue_shared/vue_resource_interceptor';
|
||||
import Poll from '../../lib/utils/poll';
|
||||
|
||||
/**
|
||||
*
|
||||
* Uses `pipelines-table-component` to render Pipelines table with an API call.
|
||||
* Endpoint is provided in HTML and passed as `endpoint`.
|
||||
* We need a store to store the received environemnts.
|
||||
* We need a service to communicate with the server.
|
||||
*
|
||||
*/
|
||||
|
||||
export default Vue.component('pipelines-table', {
|
||||
|
||||
components: {
|
||||
pipelinesTableComponent,
|
||||
errorState,
|
||||
emptyState,
|
||||
loadingIcon,
|
||||
},
|
||||
|
||||
/**
|
||||
* Accesses the DOM to provide the needed data.
|
||||
* Returns the necessary props to render `pipelines-table-component` component.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
data() {
|
||||
const store = new PipelineStore();
|
||||
|
||||
return {
|
||||
endpoint: null,
|
||||
helpPagePath: null,
|
||||
store,
|
||||
state: store.state,
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
isMakingRequest: false,
|
||||
updateGraphDropdown: false,
|
||||
hasMadeRequest: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
shouldRenderErrorState() {
|
||||
return this.hasError && !this.isLoading;
|
||||
},
|
||||
|
||||
/**
|
||||
* Empty state is only rendered if after the first request we receive no pipelines.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
shouldRenderEmptyState() {
|
||||
return !this.state.pipelines.length &&
|
||||
!this.isLoading &&
|
||||
this.hasMadeRequest &&
|
||||
!this.hasError;
|
||||
},
|
||||
|
||||
shouldRenderTable() {
|
||||
return !this.isLoading &&
|
||||
this.state.pipelines.length > 0 &&
|
||||
!this.hasError;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* When the component is about to be mounted, tell the service to fetch the data
|
||||
*
|
||||
* A request to fetch the pipelines will be made.
|
||||
* In case of a successfull response we will store the data in the provided
|
||||
* store, in case of a failed response we need to warn the user.
|
||||
*
|
||||
*/
|
||||
beforeMount() {
|
||||
const element = document.querySelector('#commit-pipeline-table-view');
|
||||
|
||||
this.endpoint = element.dataset.endpoint;
|
||||
this.helpPagePath = element.dataset.helpPagePath;
|
||||
this.service = new PipelinesService(this.endpoint);
|
||||
|
||||
this.poll = new Poll({
|
||||
resource: this.service,
|
||||
method: 'getPipelines',
|
||||
successCallback: this.successCallback,
|
||||
errorCallback: this.errorCallback,
|
||||
notificationCallback: this.setIsMakingRequest,
|
||||
});
|
||||
|
||||
if (!Visibility.hidden()) {
|
||||
this.isLoading = true;
|
||||
this.poll.makeRequest();
|
||||
} else {
|
||||
// If tab is not visible we need to make the first request so we don't show the empty
|
||||
// state without knowing if there are any pipelines
|
||||
this.fetchPipelines();
|
||||
}
|
||||
|
||||
Visibility.change(() => {
|
||||
if (!Visibility.hidden()) {
|
||||
this.poll.restart();
|
||||
} else {
|
||||
this.poll.stop();
|
||||
}
|
||||
});
|
||||
|
||||
eventHub.$on('refreshPipelines', this.fetchPipelines);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
eventHub.$off('refreshPipelines');
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.poll.stop();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchPipelines() {
|
||||
this.isLoading = true;
|
||||
|
||||
return this.service.getPipelines()
|
||||
.then(response => this.successCallback(response))
|
||||
.catch(() => this.errorCallback());
|
||||
},
|
||||
|
||||
successCallback(resp) {
|
||||
const response = resp.json();
|
||||
|
||||
this.hasMadeRequest = true;
|
||||
|
||||
// depending of the endpoint the response can either bring a `pipelines` key or not.
|
||||
const pipelines = response.pipelines || response;
|
||||
this.store.storePipelines(pipelines);
|
||||
this.isLoading = false;
|
||||
this.updateGraphDropdown = true;
|
||||
},
|
||||
|
||||
errorCallback() {
|
||||
this.hasError = true;
|
||||
this.isLoading = false;
|
||||
this.updateGraphDropdown = false;
|
||||
},
|
||||
|
||||
setIsMakingRequest(isMakingRequest) {
|
||||
this.isMakingRequest = isMakingRequest;
|
||||
|
||||
if (isMakingRequest) {
|
||||
this.updateGraphDropdown = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<div class="content-list pipelines">
|
||||
|
||||
<loading-icon
|
||||
label="Loading pipelines"
|
||||
size="3"
|
||||
v-if="isLoading"
|
||||
/>
|
||||
|
||||
<empty-state
|
||||
v-if="shouldRenderEmptyState"
|
||||
:help-page-path="helpPagePath" />
|
||||
|
||||
<error-state v-if="shouldRenderErrorState" />
|
||||
|
||||
<div
|
||||
class="table-holder"
|
||||
v-if="shouldRenderTable">
|
||||
<pipelines-table-component
|
||||
:pipelines="state.pipelines"
|
||||
:service="service"
|
||||
:update-graph-dropdown="updateGraphDropdown"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
|
@ -0,0 +1,90 @@
|
|||
<script>
|
||||
import PipelinesService from '../../pipelines/services/pipelines_service';
|
||||
import PipelineStore from '../../pipelines/stores/pipelines_store';
|
||||
import pipelinesMixin from '../../pipelines/mixins/pipelines';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
endpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
helpPagePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
mixins: [
|
||||
pipelinesMixin,
|
||||
],
|
||||
|
||||
data() {
|
||||
const store = new PipelineStore();
|
||||
|
||||
return {
|
||||
store,
|
||||
state: store.state,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Empty state is only rendered if after the first request we receive no pipelines.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
shouldRenderEmptyState() {
|
||||
return !this.state.pipelines.length &&
|
||||
!this.isLoading &&
|
||||
this.hasMadeRequest &&
|
||||
!this.hasError;
|
||||
},
|
||||
|
||||
shouldRenderTable() {
|
||||
return !this.isLoading &&
|
||||
this.state.pipelines.length > 0 &&
|
||||
!this.hasError;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.service = new PipelinesService(this.endpoint);
|
||||
},
|
||||
methods: {
|
||||
successCallback(resp) {
|
||||
const response = resp.json();
|
||||
|
||||
// depending of the endpoint the response can either bring a `pipelines` key or not.
|
||||
const pipelines = response.pipelines || response;
|
||||
this.setCommonData(pipelines);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="content-list pipelines">
|
||||
|
||||
<loading-icon
|
||||
label="Loading pipelines"
|
||||
size="3"
|
||||
v-if="isLoading"
|
||||
/>
|
||||
|
||||
<empty-state
|
||||
v-if="shouldRenderEmptyState"
|
||||
:help-page-path="helpPagePath"
|
||||
/>
|
||||
|
||||
<error-state
|
||||
v-if="shouldRenderErrorState"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="table-holder"
|
||||
v-if="shouldRenderTable">
|
||||
<pipelines-table-component
|
||||
:pipelines="state.pipelines"
|
||||
:update-graph-dropdown="updateGraphDropdown"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -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;
|
||||
|
@ -79,7 +80,18 @@ import initSettingsPanels from './settings_panels';
|
|||
path = page.split(':');
|
||||
shortcut_handler = null;
|
||||
|
||||
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
|
||||
$('.js-gfm-input').each((i, el) => {
|
||||
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
|
||||
const enableGFM = gl.utils.convertPermissionToBoolean(el.dataset.supportsAutocomplete);
|
||||
gfm.setup($(el), {
|
||||
emojis: true,
|
||||
members: enableGFM,
|
||||
issues: enableGFM,
|
||||
milestones: enableGFM,
|
||||
mergeRequests: enableGFM,
|
||||
labels: enableGFM,
|
||||
});
|
||||
});
|
||||
|
||||
function initBlob() {
|
||||
new LineHighlighter();
|
||||
|
@ -109,6 +121,9 @@ import initSettingsPanels from './settings_panels';
|
|||
}
|
||||
|
||||
switch (page) {
|
||||
case 'profiles:preferences:show':
|
||||
initExperimentalFlags();
|
||||
break;
|
||||
case 'sessions:new':
|
||||
new UsernameValidator();
|
||||
new ActiveTabMemoizer();
|
||||
|
@ -176,7 +191,7 @@ import initSettingsPanels from './settings_panels';
|
|||
case 'groups:milestones:update':
|
||||
new ZenMode();
|
||||
new gl.DueDateSelectors();
|
||||
new gl.GLForm($('.milestone-form'));
|
||||
new gl.GLForm($('.milestone-form'), true);
|
||||
break;
|
||||
case 'projects:compare:show':
|
||||
new gl.Diff();
|
||||
|
@ -188,18 +203,18 @@ import initSettingsPanels from './settings_panels';
|
|||
case 'projects:issues:new':
|
||||
case 'projects:issues:edit':
|
||||
shortcut_handler = new ShortcutsNavigation();
|
||||
new gl.GLForm($('.issue-form'));
|
||||
new gl.GLForm($('.issue-form'), true);
|
||||
new IssuableForm($('.issue-form'));
|
||||
new LabelsSelect();
|
||||
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();
|
||||
new gl.GLForm($('.merge-request-form'));
|
||||
new gl.GLForm($('.merge-request-form'), true);
|
||||
new IssuableForm($('.merge-request-form'));
|
||||
new LabelsSelect();
|
||||
new MilestoneSelect();
|
||||
|
@ -208,32 +223,30 @@ import initSettingsPanels from './settings_panels';
|
|||
break;
|
||||
case 'projects:tags:new':
|
||||
new ZenMode();
|
||||
new gl.GLForm($('.tag-form'));
|
||||
new gl.GLForm($('.tag-form'), true);
|
||||
new RefSelectDropdown($('.js-branch-select'), window.gl.availableRefs);
|
||||
break;
|
||||
case 'projects:snippets:new':
|
||||
case 'projects:snippets:edit':
|
||||
case 'projects:snippets:create':
|
||||
case 'projects:snippets:update':
|
||||
new gl.GLForm($('.snippet-form'), true);
|
||||
break;
|
||||
case 'snippets:new':
|
||||
case 'snippets:edit':
|
||||
case 'snippets:create':
|
||||
case 'snippets:update':
|
||||
new gl.GLForm($('.snippet-form'));
|
||||
new gl.GLForm($('.snippet-form'), false);
|
||||
break;
|
||||
case 'projects:releases:edit':
|
||||
new ZenMode();
|
||||
new gl.GLForm($('.release-form'));
|
||||
new gl.GLForm($('.release-form'), true);
|
||||
break;
|
||||
case 'projects:merge_requests:show':
|
||||
new gl.Diff();
|
||||
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;
|
||||
|
@ -302,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();
|
||||
|
@ -369,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();
|
||||
|
@ -379,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':
|
||||
|
@ -471,7 +484,7 @@ import initSettingsPanels from './settings_panels';
|
|||
new gl.Wikis();
|
||||
shortcut_handler = new ShortcutsWiki();
|
||||
new ZenMode();
|
||||
new gl.GLForm($('.wiki-form'));
|
||||
new gl.GLForm($('.wiki-form'), true);
|
||||
break;
|
||||
case 'snippets':
|
||||
shortcut_handler = new ShortcutsNavigation();
|
||||
|
|
|
@ -287,6 +287,10 @@ window.DropzoneInput = (function() {
|
|||
$uploadingErrorMessage.html(message);
|
||||
};
|
||||
|
||||
closeAlertMessage = function() {
|
||||
return form.find('.div-dropzone-alert').alert('close');
|
||||
};
|
||||
|
||||
form.find('.markdown-selector').click(function(e) {
|
||||
e.preventDefault();
|
||||
$(this).closest('.gfm-form').find('.div-dropzone').click();
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -32,7 +32,6 @@ export default {
|
|||
state: store.state,
|
||||
visibility: 'available',
|
||||
isLoading: false,
|
||||
isLoadingFolderContent: false,
|
||||
cssContainerClass: environmentsData.cssClass,
|
||||
endpoint: environmentsData.environmentsDataEndpoint,
|
||||
canCreateDeployment: environmentsData.canCreateDeployment,
|
||||
|
@ -86,9 +85,6 @@ export default {
|
|||
errorCallback: this.errorCallback,
|
||||
notificationCallback: (isMakingRequest) => {
|
||||
this.isMakingRequest = isMakingRequest;
|
||||
|
||||
// We need to verify if any folder is open to also fecth it
|
||||
this.openFolders = this.store.getOpenFolders();
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -119,7 +115,7 @@ export default {
|
|||
this.store.toggleFolder(folder);
|
||||
|
||||
if (!folder.isOpen) {
|
||||
this.fetchChildEnvironments(folder, folderUrl);
|
||||
this.fetchChildEnvironments(folder, folderUrl, true);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -147,19 +143,17 @@ export default {
|
|||
.catch(this.errorCallback);
|
||||
},
|
||||
|
||||
fetchChildEnvironments(folder, folderUrl) {
|
||||
this.isLoadingFolderContent = true;
|
||||
fetchChildEnvironments(folder, folderUrl, showLoader = false) {
|
||||
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
|
||||
|
||||
this.service.getFolderContent(folderUrl)
|
||||
.then(resp => resp.json())
|
||||
.then((response) => {
|
||||
this.store.setfolderContent(folder, response.environments);
|
||||
this.isLoadingFolderContent = false;
|
||||
})
|
||||
.then(response => this.store.setfolderContent(folder, response.environments))
|
||||
.then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
|
||||
.catch(() => {
|
||||
this.isLoadingFolderContent = false;
|
||||
// eslint-disable-next-line no-new
|
||||
new Flash('An error occurred while fetching the environments.');
|
||||
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -176,13 +170,13 @@ export default {
|
|||
successCallback(resp) {
|
||||
this.saveData(resp);
|
||||
|
||||
// If folders are open while polling we need to open them again
|
||||
if (this.openFolders.length) {
|
||||
this.openFolders.map((folder) => {
|
||||
// We need to verify if any folder is open to also update it
|
||||
const openFolders = this.store.getOpenFolders();
|
||||
if (openFolders.length) {
|
||||
openFolders.forEach((folder) => {
|
||||
// TODO - Move this to the backend
|
||||
const folderUrl = `${window.location.pathname}/folders/${folder.folderName}`;
|
||||
|
||||
this.store.updateFolder(folder, 'isOpen', true);
|
||||
return this.fetchChildEnvironments(folder, folderUrl);
|
||||
});
|
||||
}
|
||||
|
@ -267,7 +261,7 @@ export default {
|
|||
:environments="state.environments"
|
||||
:can-create-deployment="canCreateDeploymentParsed"
|
||||
:can-read-environment="canReadEnvironmentParsed"
|
||||
:is-loading-folder-content="isLoadingFolderContent" />
|
||||
/>
|
||||
</div>
|
||||
|
||||
<table-pagination
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -403,6 +403,14 @@ export default {
|
|||
return '';
|
||||
},
|
||||
|
||||
displayEnvironmentActions() {
|
||||
return this.hasManualActions ||
|
||||
this.externalURL ||
|
||||
this.monitoringUrl ||
|
||||
this.hasStopAction ||
|
||||
this.canRetry;
|
||||
},
|
||||
|
||||
/**
|
||||
* Constructs folder URL based on the current location and the folder id.
|
||||
*
|
||||
|
@ -535,9 +543,12 @@ export default {
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<div class="table-section section-30 table-button-footer" role="gridcell">
|
||||
<div
|
||||
v-if="!model.isFolder && displayEnvironmentActions"
|
||||
class="table-section section-30 table-button-footer"
|
||||
role="gridcell">
|
||||
|
||||
<div
|
||||
v-if="!model.isFolder"
|
||||
class="btn-group table-action-buttons"
|
||||
role="group">
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -29,12 +29,6 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
isLoadingFolderContent: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -74,7 +68,7 @@ export default {
|
|||
/>
|
||||
|
||||
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
|
||||
<div v-if="isLoadingFolderContent">
|
||||
<div v-if="model.isLoadingFolderContent">
|
||||
<loading-icon size="2" />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -35,14 +35,18 @@ export default class EnvironmentsStore {
|
|||
*/
|
||||
storeEnvironments(environments = []) {
|
||||
const filteredEnvironments = environments.map((env) => {
|
||||
const oldEnvironmentState = this.state.environments
|
||||
.find(element => element.id === env.latest.id) || {};
|
||||
|
||||
let filtered = {};
|
||||
|
||||
if (env.size > 1) {
|
||||
filtered = Object.assign({}, env, {
|
||||
isFolder: true,
|
||||
isLoadingFolderContent: oldEnvironmentState.isLoading || false,
|
||||
folderName: env.name,
|
||||
isOpen: false,
|
||||
children: [],
|
||||
isOpen: oldEnvironmentState.isOpen || false,
|
||||
children: oldEnvironmentState.children || [],
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -98,7 +102,7 @@ export default class EnvironmentsStore {
|
|||
* @return {Array}
|
||||
*/
|
||||
toggleFolder(folder) {
|
||||
return this.updateFolder(folder, 'isOpen', !folder.isOpen);
|
||||
return this.updateEnvironmentProp(folder, 'isOpen', !folder.isOpen);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -125,23 +129,23 @@ export default class EnvironmentsStore {
|
|||
return updated;
|
||||
});
|
||||
|
||||
return this.updateFolder(folder, 'children', updatedEnvironments);
|
||||
return this.updateEnvironmentProp(folder, 'children', updatedEnvironments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a folder a prop and a new value updates the correct folder.
|
||||
* Given a environment, a prop and a new value updates the correct environment.
|
||||
*
|
||||
* @param {Object} folder
|
||||
* @param {Object} environment
|
||||
* @param {String} prop
|
||||
* @param {String|Boolean|Object|Array} newValue
|
||||
* @return {Array}
|
||||
*/
|
||||
updateFolder(folder, prop, newValue) {
|
||||
updateEnvironmentProp(environment, prop, newValue) {
|
||||
const environments = this.state.environments;
|
||||
|
||||
const updatedEnvironments = environments.map((env) => {
|
||||
const updateEnv = Object.assign({}, env);
|
||||
if (env.isFolder && env.id === folder.id) {
|
||||
if (env.id === environment.id) {
|
||||
updateEnv[prop] = newValue;
|
||||
}
|
||||
|
||||
|
@ -149,8 +153,6 @@ export default class EnvironmentsStore {
|
|||
});
|
||||
|
||||
this.state.environments = updatedEnvironments;
|
||||
|
||||
return updatedEnvironments;
|
||||
}
|
||||
|
||||
getOpenFolders() {
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -56,7 +56,7 @@ class DropdownHint extends gl.FilteredSearchDropdown {
|
|||
}
|
||||
|
||||
renderContent() {
|
||||
const dropdownData = gl.FilteredSearchTokenKeys.get()
|
||||
const dropdownData = this.tokenKeys.get()
|
||||
.map(tokenKey => ({
|
||||
icon: `fa-${tokenKey.icon}`,
|
||||
hint: tokenKey.key,
|
||||
|
|
|
@ -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) {
|
||||
|
@ -40,6 +41,10 @@ class FilteredSearchManager {
|
|||
return [];
|
||||
})
|
||||
.then((searches) => {
|
||||
if (!searches) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Put any searches that may have come in before
|
||||
// we fetched the saved searches ahead of the already saved ones
|
||||
const resultantSearches = this.recentSearchesStore.setRecentSearches(
|
||||
|
@ -223,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) {
|
||||
|
@ -487,6 +488,7 @@ class FilteredSearchManager {
|
|||
}
|
||||
|
||||
searchState(e) {
|
||||
e.preventDefault();
|
||||
const target = e.currentTarget;
|
||||
// remove focus outline after click
|
||||
target.blur();
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
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 glRegexp from './lib/utils/regexp';
|
||||
import AjaxCache from './lib/utils/ajax_cache';
|
||||
|
||||
function sanitize(str) {
|
||||
return str.replace(/<(?:.|\n)*?>/gm, '');
|
||||
|
@ -34,7 +31,7 @@ class GfmAutoComplete {
|
|||
const $input = $(input);
|
||||
$input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
|
||||
// This triggers at.js again
|
||||
// Needed for slash commands with suffixes (ex: /label ~)
|
||||
// Needed for quick actions with suffixes (ex: /label ~)
|
||||
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
|
||||
$input.on('clear-commands-cache.atwho', () => this.clearCache());
|
||||
});
|
||||
|
@ -48,8 +45,8 @@ class GfmAutoComplete {
|
|||
if (this.enableMap.mergeRequests) this.setupMergeRequests($input);
|
||||
if (this.enableMap.labels) this.setupLabels($input);
|
||||
|
||||
// We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
|
||||
$input.filter('[data-supports-slash-commands="true"]').atwho({
|
||||
// We don't instantiate the quick actions autocomplete for note and issue/MR edit forms
|
||||
$input.filter('[data-supports-quick-actions="true"]').atwho({
|
||||
at: '/',
|
||||
alias: 'commands',
|
||||
searchKey: 'search',
|
||||
|
@ -375,7 +372,12 @@ 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)));
|
||||
import(/* webpackChunkName: 'emoji' */ './emoji')
|
||||
.then(({ validEmojiNames, glEmojiTag }) => {
|
||||
this.loadData($input, at, validEmojiNames);
|
||||
GfmAutoComplete.glEmojiTag = glEmojiTag;
|
||||
})
|
||||
.catch(() => { this.isLoadingData[at] = false; });
|
||||
} else {
|
||||
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
|
||||
.then((data) => {
|
||||
|
@ -398,6 +400,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) {
|
||||
|
@ -423,12 +432,14 @@ GfmAutoComplete.atTypeMap = {
|
|||
};
|
||||
|
||||
// Emoji
|
||||
GfmAutoComplete.glEmojiTag = null;
|
||||
GfmAutoComplete.Emoji = {
|
||||
templateFunction(name) {
|
||||
return `<li>
|
||||
${name} ${glEmojiTag(name)}
|
||||
</li>
|
||||
`;
|
||||
// glEmojiTag helper is loaded on-demand in fetchData()
|
||||
if (GfmAutoComplete.glEmojiTag) {
|
||||
return `<li>${name} ${GfmAutoComplete.glEmojiTag(name)}</li>`;
|
||||
}
|
||||
return `<li>${name}</li>`;
|
||||
},
|
||||
};
|
||||
// Team Members
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
|
||||
import Cookies from 'js-cookie';
|
||||
import _ from 'underscore';
|
||||
|
||||
export default class GroupName {
|
||||
constructor() {
|
||||
this.titleContainer = document.querySelector('.title-container');
|
||||
this.title = document.querySelector('.title');
|
||||
this.titleContainer = document.querySelector('.js-title-container');
|
||||
this.title = this.titleContainer.querySelector('.title');
|
||||
this.titleWidth = this.title.offsetWidth;
|
||||
this.groupTitle = document.querySelector('.group-title');
|
||||
this.groups = document.querySelectorAll('.group-path');
|
||||
this.groupTitle = this.titleContainer.querySelector('.group-title');
|
||||
this.groups = this.titleContainer.querySelectorAll('.group-path');
|
||||
this.toggle = null;
|
||||
this.isHidden = false;
|
||||
this.init();
|
||||
|
@ -33,11 +33,20 @@ export default class GroupName {
|
|||
|
||||
createToggle() {
|
||||
this.toggle = document.createElement('button');
|
||||
this.toggle.setAttribute('type', 'button');
|
||||
this.toggle.className = 'text-expander group-name-toggle';
|
||||
this.toggle.setAttribute('aria-label', 'Toggle full path');
|
||||
this.toggle.innerHTML = '...';
|
||||
if (Cookies.get('new_nav') === 'true') {
|
||||
this.toggle.innerHTML = '<i class="fa fa-ellipsis-h" aria-hidden="true"></i>';
|
||||
} else {
|
||||
this.toggle.innerHTML = '...';
|
||||
}
|
||||
this.toggle.addEventListener('click', this.toggleGroups.bind(this));
|
||||
this.titleContainer.insertBefore(this.toggle, this.title);
|
||||
if (Cookies.get('new_nav') === 'true') {
|
||||
this.title.insertBefore(this.toggle, this.groupTitle);
|
||||
} else {
|
||||
this.titleContainer.insertBefore(this.toggle, this.title);
|
||||
}
|
||||
this.toggleGroups();
|
||||
}
|
||||
|
||||
|
|
|
@ -47,8 +47,8 @@ export default class GroupsStore {
|
|||
|
||||
// Map groups to an object
|
||||
groups.map((group) => {
|
||||
mappedGroups[group.id] = group;
|
||||
mappedGroups[group.id].subGroups = {};
|
||||
mappedGroups[`id${group.id}`] = group;
|
||||
mappedGroups[`id${group.id}`].subGroups = {};
|
||||
return group;
|
||||
});
|
||||
|
||||
|
@ -56,26 +56,27 @@ export default class GroupsStore {
|
|||
const currentGroup = mappedGroups[key];
|
||||
if (currentGroup.parentId) {
|
||||
// If the group is not at the root level, add it to its parent array of subGroups.
|
||||
const findParentGroup = mappedGroups[currentGroup.parentId];
|
||||
const findParentGroup = mappedGroups[`id${currentGroup.parentId}`];
|
||||
if (findParentGroup) {
|
||||
mappedGroups[currentGroup.parentId].subGroups[currentGroup.id] = currentGroup;
|
||||
mappedGroups[currentGroup.parentId].isOpen = true; // Expand group if it has subgroups
|
||||
mappedGroups[`id${currentGroup.parentId}`].subGroups[`id${currentGroup.id}`] = currentGroup;
|
||||
mappedGroups[`id${currentGroup.parentId}`].isOpen = true; // Expand group if it has subgroups
|
||||
} else if (parentGroup && parentGroup.id === currentGroup.parentId) {
|
||||
tree[currentGroup.id] = currentGroup;
|
||||
tree[`id${currentGroup.id}`] = currentGroup;
|
||||
} else {
|
||||
// Means the groups hast no direct parent.
|
||||
// Save for later processing, we will add them to its corresponding base group
|
||||
// No parent found. We save it for later processing
|
||||
orphans.push(currentGroup);
|
||||
|
||||
// Add to tree to preserve original order
|
||||
tree[`id${currentGroup.id}`] = currentGroup;
|
||||
}
|
||||
} else {
|
||||
// If the group is at the root level, add it to first level elements array.
|
||||
tree[currentGroup.id] = currentGroup;
|
||||
// If the group is at the top level, add it to first level elements array.
|
||||
tree[`id${currentGroup.id}`] = currentGroup;
|
||||
}
|
||||
|
||||
return key;
|
||||
});
|
||||
|
||||
// Hopefully this array will be empty for most cases
|
||||
if (orphans.length) {
|
||||
orphans.map((orphan) => {
|
||||
let found = false;
|
||||
|
@ -83,11 +84,23 @@ export default class GroupsStore {
|
|||
|
||||
Object.keys(tree).map((key) => {
|
||||
const group = tree[key];
|
||||
if (currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0) {
|
||||
|
||||
if (
|
||||
group &&
|
||||
currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0 &&
|
||||
// Make sure the currently selected orphan is not the same as the group
|
||||
// we are checking here otherwise it will end up in an infinite loop
|
||||
currentOrphan.id !== group.id
|
||||
) {
|
||||
group.subGroups[currentOrphan.id] = currentOrphan;
|
||||
group.isOpen = true;
|
||||
currentOrphan.isOrphan = true;
|
||||
found = true;
|
||||
|
||||
// Delete if group was put at the top level. If not the group will be displayed twice.
|
||||
if (tree[`id${currentOrphan.id}`]) {
|
||||
delete tree[`id${currentOrphan.id}`];
|
||||
}
|
||||
}
|
||||
|
||||
return key;
|
||||
|
@ -95,7 +108,8 @@ export default class GroupsStore {
|
|||
|
||||
if (!found) {
|
||||
currentOrphan.isOrphan = true;
|
||||
tree[currentOrphan.id] = currentOrphan;
|
||||
|
||||
tree[`id${currentOrphan.id}`] = currentOrphan;
|
||||
}
|
||||
|
||||
return orphan;
|
||||
|
@ -140,7 +154,7 @@ export default class GroupsStore {
|
|||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
removeGroup(group, collection) {
|
||||
Vue.delete(collection, group.id);
|
||||
Vue.delete(collection, `id${group.id}`);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
/* global SubscriptionSelect */
|
||||
|
||||
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
|
||||
import SidebarHeightManager from './sidebar_height_manager';
|
||||
|
||||
const HIDDEN_CLASS = 'hidden';
|
||||
const DISABLED_CONTENT_CLASS = 'disabled-content';
|
||||
|
@ -22,6 +23,7 @@ export default class IssuableBulkUpdateSidebar {
|
|||
initDomElements() {
|
||||
this.$page = $('.page-with-sidebar');
|
||||
this.$sidebar = $('.right-sidebar');
|
||||
this.$sidebarInnerContainer = this.$sidebar.find('.issuable-sidebar');
|
||||
this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide');
|
||||
this.$bulkEditSubmitBtn = $('.update-selected-issues');
|
||||
this.$bulkUpdateEnableBtn = $('.js-bulk-update-toggle');
|
||||
|
@ -55,18 +57,6 @@ export default class IssuableBulkUpdateSidebar {
|
|||
return navbarHeight + layoutNavHeight + subNavScroll;
|
||||
}
|
||||
|
||||
initSidebar() {
|
||||
if (!this.navHeight) {
|
||||
this.navHeight = this.getNavHeight();
|
||||
}
|
||||
|
||||
if (!this.sidebarInitialized) {
|
||||
$(document).off('scroll').on('scroll', _.throttle(this.setSidebarHeight, 10).bind(this));
|
||||
$(window).off('resize').on('resize', _.throttle(this.setSidebarHeight, 10).bind(this));
|
||||
this.sidebarInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
setupBulkUpdateActions() {
|
||||
IssuableBulkUpdateActions.setOriginalDropdownData();
|
||||
}
|
||||
|
@ -96,7 +86,7 @@ export default class IssuableBulkUpdateSidebar {
|
|||
this.toggleCheckboxDisplay(enable);
|
||||
|
||||
if (enable) {
|
||||
this.initSidebar();
|
||||
SidebarHeightManager.init();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,6 +103,7 @@ export default class IssuableBulkUpdateSidebar {
|
|||
toggleSidebarDisplay(show) {
|
||||
this.$page.toggleClass(SIDEBAR_EXPANDED_CLASS, show);
|
||||
this.$page.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
|
||||
this.$sidebarInnerContainer.toggleClass(HIDDEN_CLASS, !show);
|
||||
this.$sidebar.toggleClass(SIDEBAR_EXPANDED_CLASS, show);
|
||||
this.$sidebar.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
|
||||
}
|
||||
|
@ -141,17 +132,6 @@ export default class IssuableBulkUpdateSidebar {
|
|||
this.$bulkEditSubmitBtn.enable();
|
||||
}
|
||||
}
|
||||
// loosely based on method of the same name in right_sidebar.js
|
||||
setSidebarHeight() {
|
||||
const currentScrollDepth = window.pageYOffset || 0;
|
||||
const diff = this.navHeight - currentScrollDepth;
|
||||
|
||||
if (diff > 0) {
|
||||
this.$sidebar.outerHeight(window.innerHeight - diff);
|
||||
} else {
|
||||
this.$sidebar.outerHeight('100%');
|
||||
}
|
||||
}
|
||||
|
||||
static getCheckedIssueIds() {
|
||||
const $checkedIssues = $('.selected_issue:checked');
|
||||
|
|
|
@ -51,6 +51,11 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
initialTaskStatus: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
updatedAt: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -105,6 +110,7 @@ export default {
|
|||
updatedAt: this.updatedAt,
|
||||
updatedByName: this.updatedByName,
|
||||
updatedByPath: this.updatedByPath,
|
||||
taskStatus: this.initialTaskStatus,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -198,13 +204,7 @@ export default {
|
|||
method: 'getData',
|
||||
successCallback: (res) => {
|
||||
const data = res.json();
|
||||
const shouldUpdate = this.store.stateShouldUpdate(data);
|
||||
|
||||
this.store.updateState(data);
|
||||
|
||||
if (this.showForm && (shouldUpdate.title || shouldUpdate.description)) {
|
||||
this.store.formState.lockedWarningVisible = true;
|
||||
}
|
||||
},
|
||||
errorCallback(err) {
|
||||
throw new Error(err);
|
||||
|
|
|
@ -37,7 +37,24 @@
|
|||
});
|
||||
},
|
||||
taskStatus() {
|
||||
const taskRegexMatches = this.taskStatus.match(/(\d+) of (\d+)/);
|
||||
this.updateTaskStatusText();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
renderGFM() {
|
||||
$(this.$refs['gfm-content']).renderGFM();
|
||||
|
||||
if (this.canUpdate) {
|
||||
// eslint-disable-next-line no-new
|
||||
new gl.TaskList({
|
||||
dataType: 'issue',
|
||||
fieldName: 'description',
|
||||
selector: '.detail-page-description',
|
||||
});
|
||||
}
|
||||
},
|
||||
updateTaskStatusText() {
|
||||
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
|
||||
const $issuableHeader = $('.issuable-meta');
|
||||
const $tasks = $('#task_status', $issuableHeader);
|
||||
const $tasksShort = $('#task_status_short', $issuableHeader);
|
||||
|
@ -51,22 +68,9 @@
|
|||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
renderGFM() {
|
||||
$(this.$refs['gfm-entry-content']).renderGFM();
|
||||
|
||||
if (this.canUpdate) {
|
||||
// eslint-disable-next-line no-new
|
||||
new gl.TaskList({
|
||||
dataType: 'issue',
|
||||
fieldName: 'description',
|
||||
selector: '.detail-page-description',
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.renderGFM();
|
||||
this.updateTaskStatusText();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -41,13 +41,14 @@
|
|||
<textarea
|
||||
id="issue-description"
|
||||
class="note-textarea js-gfm-input js-autosize markdown-area"
|
||||
data-supports-slash-commands="false"
|
||||
data-supports-quick-actionss="false"
|
||||
aria-label="Description"
|
||||
v-model="formState.description"
|
||||
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>
|
||||
|
|
|
@ -45,6 +45,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
updatedAt: this.updatedAt,
|
||||
updatedByName: this.updatedByName,
|
||||
updatedByPath: this.updatedByPath,
|
||||
initialTaskStatus: this.initialTaskStatus,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,23 +1,6 @@
|
|||
export default class Store {
|
||||
constructor({
|
||||
titleHtml,
|
||||
titleText,
|
||||
descriptionHtml,
|
||||
descriptionText,
|
||||
updatedAt,
|
||||
updatedByName,
|
||||
updatedByPath,
|
||||
}) {
|
||||
this.state = {
|
||||
titleHtml,
|
||||
titleText,
|
||||
descriptionHtml,
|
||||
descriptionText,
|
||||
taskStatus: '',
|
||||
updatedAt,
|
||||
updatedByName,
|
||||
updatedByPath,
|
||||
};
|
||||
constructor(initialState) {
|
||||
this.state = initialState;
|
||||
this.formState = {
|
||||
title: '',
|
||||
confidential: false,
|
||||
|
@ -29,6 +12,10 @@ export default class Store {
|
|||
}
|
||||
|
||||
updateState(data) {
|
||||
if (this.stateShouldUpdate(data)) {
|
||||
this.formState.lockedWarningVisible = true;
|
||||
}
|
||||
|
||||
this.state.titleHtml = data.title;
|
||||
this.state.titleText = data.title_text;
|
||||
this.state.descriptionHtml = data.description;
|
||||
|
@ -40,10 +27,8 @@ export default class Store {
|
|||
}
|
||||
|
||||
stateShouldUpdate(data) {
|
||||
return {
|
||||
title: this.state.titleText !== data.title_text,
|
||||
description: this.state.descriptionText !== data.description_text,
|
||||
};
|
||||
return this.state.titleText !== data.title_text ||
|
||||
this.state.descriptionText !== data.description_text;
|
||||
}
|
||||
|
||||
setFormState(state) {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -26,14 +26,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
mounted() {
|
||||
this.mediator.initBuildClass();
|
||||
},
|
||||
updated() {
|
||||
// Wait for flash message to be appended
|
||||
Vue.nextTick(() => {
|
||||
if (this.mediator.build) {
|
||||
this.mediator.build.verifyTopPosition();
|
||||
}
|
||||
});
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('job-header', {
|
||||
props: {
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
|
|
@ -86,18 +86,25 @@
|
|||
// This is required to handle non-unicode characters in hash
|
||||
hash = decodeURIComponent(hash);
|
||||
|
||||
var fixedTabs = document.querySelector('.js-tabs-affix');
|
||||
var fixedNav = document.querySelector('.navbar-gitlab');
|
||||
|
||||
var adjustment = 0;
|
||||
if (fixedNav) adjustment -= fixedNav.offsetHeight;
|
||||
|
||||
// scroll to user-generated markdown anchor if we cannot find a match
|
||||
if (document.getElementById(hash) === null) {
|
||||
var target = document.getElementById('user-content-' + hash);
|
||||
if (target && target.scrollIntoView) {
|
||||
target.scrollIntoView(true);
|
||||
window.scrollBy(0, adjustment);
|
||||
}
|
||||
} else {
|
||||
// only adjust for fixedTabs when not targeting user-generated content
|
||||
var fixedTabs = document.querySelector('.js-tabs-affix');
|
||||
if (fixedTabs) {
|
||||
window.scrollBy(0, -fixedTabs.offsetHeight);
|
||||
adjustment -= fixedTabs.offsetHeight;
|
||||
}
|
||||
window.scrollBy(0, adjustment);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ window.dateFormat = dateFormat;
|
|||
|
||||
w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) {
|
||||
$timeagoEls.each((i, el) => {
|
||||
el.setAttribute('title', gl.utils.formatDate(el.getAttribute('datetime')));
|
||||
el.setAttribute('title', el.getAttribute('title'));
|
||||
|
||||
if (setTimeago) {
|
||||
// Recreate with custom template
|
||||
|
@ -112,29 +112,11 @@ window.dateFormat = dateFormat;
|
|||
return timefor;
|
||||
};
|
||||
|
||||
w.gl.utils.cachedTimeagoElements = [];
|
||||
w.gl.utils.renderTimeago = function($els) {
|
||||
if (!$els && !w.gl.utils.cachedTimeagoElements.length) {
|
||||
w.gl.utils.cachedTimeagoElements = [].slice.call(document.querySelectorAll('.js-timeago-render'));
|
||||
} else if ($els) {
|
||||
w.gl.utils.cachedTimeagoElements = w.gl.utils.cachedTimeagoElements.concat($els.toArray());
|
||||
}
|
||||
const timeagoEls = $els || document.querySelectorAll('.js-timeago-render');
|
||||
|
||||
w.gl.utils.cachedTimeagoElements.forEach(gl.utils.updateTimeagoText);
|
||||
};
|
||||
|
||||
w.gl.utils.updateTimeagoText = function(el) {
|
||||
const formattedDate = gl.utils.getTimeago().format(el.getAttribute('datetime'), lang);
|
||||
|
||||
if (el.textContent !== formattedDate) {
|
||||
el.textContent = formattedDate;
|
||||
}
|
||||
};
|
||||
|
||||
w.gl.utils.initTimeagoTimeout = function() {
|
||||
gl.utils.renderTimeago();
|
||||
|
||||
gl.utils.timeagoTimeout = setTimeout(gl.utils.initTimeagoTimeout, 1000);
|
||||
// timeago.js sets timeouts internally for each timeago value to be updated in real time
|
||||
gl.utils.getTimeago().render(timeagoEls, lang);
|
||||
};
|
||||
|
||||
w.gl.utils.getDayDifference = function(a, b) {
|
||||
|
|
|
@ -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
|
@ -1 +0,0 @@
|
|||
var locales = locales || {}; locales['en'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-04-12 22:36-0500","Last-Translator":"FULL NAME <EMAIL@ADDRESS>","Language-Team":"English","Language":"en","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","lang":"en","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":[""],"Cancel":[""],"Commit":["",""],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":[""],"CycleAnalyticsStage|Code":[""],"CycleAnalyticsStage|Issue":[""],"CycleAnalyticsStage|Plan":[""],"CycleAnalyticsStage|Production":[""],"CycleAnalyticsStage|Review":[""],"CycleAnalyticsStage|Staging":[""],"CycleAnalyticsStage|Test":[""],"Delete":[""],"Deploy":["",""],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":[""],"FirstPushedBy|pushed by":[""],"From issue creation until deploy to production":[""],"From merge request merge until deploy to production":[""],"Interval Pattern":[""],"Introducing Cycle Analytics":[""],"Last %d day":["",""],"Last Pipeline":[""],"Limited to showing %d event at most":["",""],"Median":[""],"New Issue":["",""],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":[""],"Not enough data":[""],"OpenedNDaysAgo|Opened":[""],"Owner":[""],"Pipeline Health":[""],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":[""],"Read more":[""],"Related Commits":[""],"Related Deployed Jobs":[""],"Related Issues":[""],"Related Jobs":[""],"Related Merge Requests":[""],"Related Merged Requests":[""],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["",""],"Target Branch":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":[""],"The collection of events added to the data gathered for that stage.":[""],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":[""],"The phase of the development lifecycle.":[""],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":[""],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":[""],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":[""],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":[""],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":[""],"The time taken by each data entry gathered by that stage.":[""],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":[""],"Time before an issue gets scheduled":[""],"Time before an issue starts implementation":[""],"Time between merge request creation and merge/close":[""],"Time until first merge request":[""],"Time|hr":["",""],"Time|min":["",""],"Time|s":[""],"Total Time":[""],"Total test time for all commits/merges":[""],"Want to see the data? Please ask an administrator for access.":[""],"We don't have enough data to show this stage.":[""],"You need permission.":[""],"day":["",""]}}};
|
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
|
@ -70,7 +70,7 @@ import './ajax_loading_spinner';
|
|||
import './api';
|
||||
import './aside';
|
||||
import './autosave';
|
||||
import AwardsHandler from './awards_handler';
|
||||
import loadAwardsHandler from './awards_handler';
|
||||
import './breakpoints';
|
||||
import './broadcast_message';
|
||||
import './build';
|
||||
|
@ -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
|
||||
|
@ -354,10 +355,10 @@ $(function () {
|
|||
$window.off('resize.app').on('resize.app', function () {
|
||||
return fitSidebarForSize();
|
||||
});
|
||||
gl.awardsHandler = new AwardsHandler();
|
||||
loadAwardsHandler();
|
||||
new Aside();
|
||||
|
||||
gl.utils.initTimeagoTimeout();
|
||||
gl.utils.renderTimeago();
|
||||
|
||||
$(document).trigger('init.scrolling-tabs');
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
@ -155,7 +157,10 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
|||
|
||||
scrollToElement(container) {
|
||||
if (location.hash) {
|
||||
const offset = -$('.js-tabs-affix').outerHeight();
|
||||
const offset = 0 - (
|
||||
$('.navbar-gitlab').outerHeight() +
|
||||
$('.js-tabs-affix').outerHeight()
|
||||
);
|
||||
const $el = $(`${container} ${location.hash}:not(.match)`);
|
||||
if ($el.length) {
|
||||
$.scrollTo($el[0], { offset });
|
||||
|
@ -165,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
|
||||
|
@ -182,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"
|
||||
|
@ -191,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}`;
|
||||
}
|
||||
|
||||
|
@ -233,11 +237,18 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
|||
}
|
||||
|
||||
mountPipelinesView() {
|
||||
this.commitPipelinesTable = new gl.CommitPipelinesTable().$mount();
|
||||
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
|
||||
const CommitPipelinesTable = gl.CommitPipelinesTable;
|
||||
this.commitPipelinesTable = new CommitPipelinesTable({
|
||||
propsData: {
|
||||
endpoint: pipelineTableViewEl.dataset.endpoint,
|
||||
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
// $mount(el) replaces the el with the new rendered component. We need it in order to mount
|
||||
// it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount
|
||||
document.querySelector('#commit-pipeline-table-view')
|
||||
.appendChild(this.commitPipelinesTable.$el);
|
||||
pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el);
|
||||
}
|
||||
|
||||
loadDiff(source) {
|
||||
|
@ -284,7 +295,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
|||
// Scroll any linked note into view
|
||||
// Similar to `toggler_behavior` in the discussion tab
|
||||
const hash = window.gl.utils.getLocationHash();
|
||||
const anchor = hash && $container.find(`[id="${hash}"]`);
|
||||
const anchor = hash && $container.find(`.note[id="${hash}"]`);
|
||||
if (anchor && anchor.length > 0) {
|
||||
const notesContent = anchor.closest('.notes_content');
|
||||
const lineType = notesContent.hasClass('new') ? 'new' : 'old';
|
||||
|
@ -294,6 +305,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
|||
forceShow: true,
|
||||
});
|
||||
anchor[0].scrollIntoView();
|
||||
window.gl.utils.handleLocationHash();
|
||||
// We have multiple elements on the page with `#note_xxx`
|
||||
// (discussion and diff tabs) and `:target` only applies to the first
|
||||
anchor.addClass('target');
|
||||
|
|
|
@ -4,87 +4,7 @@
|
|||
|
||||
(function() {
|
||||
this.Milestone = (function() {
|
||||
Milestone.updateIssue = function(li, issue_url, data) {
|
||||
return $.ajax({
|
||||
type: "PUT",
|
||||
url: issue_url,
|
||||
data: data,
|
||||
success: function(_data) {
|
||||
return Milestone.successCallback(_data, li);
|
||||
},
|
||||
error: function(data) {
|
||||
return new Flash("Issue update failed", 'alert');
|
||||
},
|
||||
dataType: "json"
|
||||
});
|
||||
};
|
||||
|
||||
Milestone.sortIssues = function(url, data) {
|
||||
return $.ajax({
|
||||
type: "PUT",
|
||||
url,
|
||||
data: data,
|
||||
success: function(_data) {
|
||||
return Milestone.successCallback(_data);
|
||||
},
|
||||
error: function() {
|
||||
return new Flash("Issues update failed", 'alert');
|
||||
},
|
||||
dataType: "json"
|
||||
});
|
||||
};
|
||||
|
||||
Milestone.sortMergeRequests = function(url, data) {
|
||||
return $.ajax({
|
||||
type: "PUT",
|
||||
url,
|
||||
data: data,
|
||||
success: function(_data) {
|
||||
return Milestone.successCallback(_data);
|
||||
},
|
||||
error: function(data) {
|
||||
return new Flash("Issue update failed", 'alert');
|
||||
},
|
||||
dataType: "json"
|
||||
});
|
||||
};
|
||||
|
||||
Milestone.updateMergeRequest = function(li, merge_request_url, data) {
|
||||
return $.ajax({
|
||||
type: "PUT",
|
||||
url: merge_request_url,
|
||||
data: data,
|
||||
success: function(_data) {
|
||||
return Milestone.successCallback(_data, li);
|
||||
},
|
||||
error: function(data) {
|
||||
return new Flash("Issue update failed", 'alert');
|
||||
},
|
||||
dataType: "json"
|
||||
});
|
||||
};
|
||||
|
||||
Milestone.successCallback = function(data, element) {
|
||||
const $avatarContainer = $(element).find('.assignee-icon');
|
||||
$avatarContainer.empty();
|
||||
|
||||
if (data.assignees && data.assignees.length > 0) {
|
||||
const $avatars = data.assignees.map((assignee) => {
|
||||
const img_tag = $('<img/>');
|
||||
img_tag.attr('src', assignee.avatar_url);
|
||||
img_tag.addClass('avatar s16');
|
||||
return img_tag;
|
||||
});
|
||||
|
||||
$avatarContainer.append($avatars);
|
||||
}
|
||||
};
|
||||
|
||||
function Milestone() {
|
||||
this.issuesSortEndpoint = $('#tab-issues').data('sort-endpoint');
|
||||
this.mergeRequestsSortEndpoint = $('#tab-merge-requests').data('sort-endpoint');
|
||||
|
||||
this.bindIssuesSorting();
|
||||
this.bindTabsSwitching();
|
||||
|
||||
// Load merge request tab if it is active
|
||||
|
@ -94,22 +14,6 @@
|
|||
this.loadInitialTab();
|
||||
}
|
||||
|
||||
Milestone.prototype.bindIssuesSorting = function() {
|
||||
if (!this.issuesSortEndpoint) return;
|
||||
|
||||
$('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) {
|
||||
this.createSortable(el, {
|
||||
group: 'issue-list',
|
||||
listEls: $('.issues-sortable-list'),
|
||||
fieldName: 'issue',
|
||||
sortCallback: (data) => {
|
||||
Milestone.sortIssues(this.issuesSortEndpoint, data);
|
||||
},
|
||||
updateCallback: Milestone.updateIssue,
|
||||
});
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
Milestone.prototype.bindTabsSwitching = function() {
|
||||
return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
|
||||
const $target = $(e.target);
|
||||
|
@ -119,69 +23,6 @@
|
|||
});
|
||||
};
|
||||
|
||||
Milestone.prototype.bindMergeRequestSorting = function() {
|
||||
if (!this.mergeRequestsSortEndpoint) return;
|
||||
|
||||
$("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) {
|
||||
this.createSortable(el, {
|
||||
group: 'merge-request-list',
|
||||
listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"),
|
||||
fieldName: 'merge_request',
|
||||
sortCallback: (data) => {
|
||||
Milestone.sortMergeRequests(this.mergeRequestsSortEndpoint, data);
|
||||
},
|
||||
updateCallback: Milestone.updateMergeRequest,
|
||||
});
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
Milestone.prototype.createSortable = function(el, opts) {
|
||||
return Sortable.create(el, {
|
||||
group: opts.group,
|
||||
filter: '.is-disabled',
|
||||
forceFallback: true,
|
||||
onStart: function(e) {
|
||||
opts.listEls.css('min-height', e.item.offsetHeight);
|
||||
},
|
||||
onEnd: function () {
|
||||
opts.listEls.css("min-height", "0px");
|
||||
},
|
||||
onUpdate: function(e) {
|
||||
var ids = this.toArray(),
|
||||
data;
|
||||
|
||||
if (ids.length) {
|
||||
data = ids.map(function(id) {
|
||||
return 'sortable_' + opts.fieldName + '[]=' + id;
|
||||
}).join('&');
|
||||
|
||||
opts.sortCallback(data);
|
||||
}
|
||||
},
|
||||
onAdd: function (e) {
|
||||
var data, issuableId, issuableUrl, newState;
|
||||
newState = e.to.dataset.state;
|
||||
issuableUrl = e.item.dataset.url;
|
||||
data = (function() {
|
||||
switch (newState) {
|
||||
case 'ongoing':
|
||||
return `${opts.fieldName}[assignee_ids][]=${gon.current_user_id}`;
|
||||
case 'unassigned':
|
||||
return `${opts.fieldName}[assignee_ids][]=0`;
|
||||
case 'closed':
|
||||
return opts.fieldName + '[state_event]=close';
|
||||
}
|
||||
})();
|
||||
if (e.from.dataset.state === 'closed') {
|
||||
data += '&' + opts.fieldName + '[state_event]=reopen';
|
||||
}
|
||||
|
||||
opts.updateCallback(e.item, issuableUrl, data);
|
||||
this.options.onUpdate.call(this, e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Milestone.prototype.loadInitialTab = function() {
|
||||
const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`);
|
||||
|
||||
|
@ -203,10 +44,6 @@
|
|||
.done((data) => {
|
||||
$(tabElId).html(data.html);
|
||||
$target.addClass('is-loaded');
|
||||
|
||||
if (tabElId === '#tab-merge-requests') {
|
||||
this.bindMergeRequestSorting();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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,293 @@
|
|||
<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: 450,
|
||||
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 {
|
||||
paddingBottom: `${(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_label || '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 = 450;
|
||||
this.graphWidth = 600;
|
||||
this.measurements = measurements.large;
|
||||
this.draw();
|
||||
eventHub.$emit('toggleAspectRatio');
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.draw();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
:class="classType">
|
||||
<h5
|
||||
class="text-center graph-title">
|
||||
{{columnData.title}}
|
||||
</h5>
|
||||
<div
|
||||
class="prometheus-svg-container"
|
||||
:style="paddingBottomRootSvg">
|
||||
<svg
|
||||
:viewBox="outterViewBox"
|
||||
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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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,
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue