Compare commits
380 Commits
master
...
weekly-con
Author | SHA1 | Date |
---|---|---|
R. Tyler Croy | f05bcfef83 | |
Keith Zantow | d57bb210c7 | |
Ivan Meredith | 9574ed6b06 | |
Vivek Pandey | 623935beab | |
Vivek Pandey | 057e34bf1a | |
Vivek Pandey | 45bf09a359 | |
James William Dumay | f7b63b40b1 | |
Cliff Meyers | fe65e25e6a | |
Thorsten Scherler | 962d6a9a53 | |
Tom Fennelly | 68d5f0bd92 | |
Ivan Meredith | dee7566562 | |
Ivan Meredith | 4779755e2c | |
Tom Fennelly | c5555ff84f | |
James William Dumay | 9bdb6e39a1 | |
Tom Fennelly | 673cd48d57 | |
Nicolas Ménard | da55ff39c5 | |
Cliff Meyers | 52f2ea7fab | |
Josh McDonald | 0ce8479cc1 | |
Vivek Pandey | c1ca81c98d | |
Thorsten Scherler | 2fe57cb944 | |
Thorsten Scherler | 7e42efa005 | |
Nicolas Ménard | 7d040148ad | |
Cliff Meyers | ecfaab3426 | |
Vivek Pandey | 9d708122fd | |
Vivek Pandey | 71b813e2c6 | |
Cliff Meyers | 873c0c2f85 | |
Cliff Meyers | d79fc44735 | |
Vivek Pandey | 6191fdfed8 | |
Cliff Meyers | b5b4fdfb0e | |
Vivek Pandey | 556a5d56e6 | |
Vivek Pandey | f6cbe5f9f8 | |
vivek | ad98a3ed44 | |
Thorsten Scherler | 917c232192 | |
Thorsten Scherler | d619bdbf5c | |
Michael Neale | 54547fd93a | |
Michael Neale | 00384c1da0 | |
Tom Fennelly | e25ae38a26 | |
Ivan Meredith | dc4e9006dc | |
Michael Neale | 8755cc193e | |
Ivan Meredith | 9bf0b0c45f | |
Yoann Dubreuil | 38163fb388 | |
Sujeevan Vijayakumaran | cb3cffcb95 | |
Sujeevan Vijayakumaran | 7bd50f2474 | |
James William Dumay | f8a134cc04 | |
Ivan Meredith | 458c2a363c | |
Michael Neale | 6f0c796f3a | |
Michael Neale | 57f3219f1f | |
Ivan Meredith | c4ab96013e | |
Ivan Meredith | b2cd597a1d | |
vivek | e47c2bd0f4 | |
Tom Fennelly | 63c876b598 | |
James William Dumay | dff8076cf4 | |
James William Dumay | d68a909932 | |
vivek | b7c8316381 | |
Tom Fennelly | c584c6a2ed | |
Keith Zantow | f42d71cc1b | |
Thorsten Scherler | 7a5feac040 | |
Josh McDonald | 296ed38541 | |
Tom Fennelly | 2a0ae40645 | |
James William Dumay | 4b44e6dcee | |
James William Dumay | 969f1a189c | |
Tom Fennelly | e41c758e12 | |
Yoann Dubreuil | 6283c1375e | |
Cliff Meyers | 2dd5cca04c | |
Ivan Meredith | ca2bdb08b4 | |
Cliff Meyers | 2c7f1c6790 | |
Thorsten Scherler | 1c13b6393b | |
James William Dumay | f0e9fde8c3 | |
Thorsten Scherler | b9ee8603dd | |
Thorsten Scherler | 87faf166ad | |
Ivan Meredith | 7be34f65cf | |
Ivan Meredith | 8600c1b91d | |
Cliff Meyers | 1a576d3c11 | |
Thorsten Scherler | e8b7e06a6d | |
Ivan Meredith | b2877037ff | |
Alexander Shorin | fab21e9c1e | |
Cliff Meyers | 5de4d5c846 | |
vivek | 5715b8aaf0 | |
vivek | f4e549bc9c | |
Thorsten Scherler | 26e99db91a | |
Thorsten Scherler | d1a80909c9 | |
Josh McDonald | 81cc2eba57 | |
Michael Neale | affc7bfee4 | |
vivek | 6b743eafd1 | |
James William Dumay | 648e6579f0 | |
Thorsten Scherler | b5c311b488 | |
Thorsten Scherler | ed76c2cb72 | |
Keith Zantow | 1ce912340a | |
James William Dumay | e29025df6a | |
Michael Neale | b0d66b6b7d | |
Tom Fennelly | 89aee848ad | |
Vivek Pandey | 81a67eac66 | |
Vivek Pandey | d556c1ed52 | |
Ivan Meredith | 3f283a2360 | |
Alexander Shorin | e8955bfcf8 | |
James Dumay | f1eef22c4d | |
Ivan Meredith | 826cb28bb2 | |
Josh McDonald | 5c356b1d9a | |
James Dumay | 953a8421d1 | |
James Dumay | 474ae49dd4 | |
James Dumay | 94cd748cd3 | |
James Dumay | da1f25b662 | |
Thorsten Scherler | 1ba6c8edb0 | |
James Dumay | 7b7d2858e5 | |
Max Knee | e0d86c083c | |
James Dumay | f8750cc0e7 | |
James William Dumay | 8ce8915b04 | |
James William Dumay | 5377fb6038 | |
Michael Neale | 1688221437 | |
Tom Fennelly | 6872affa3e | |
Scott Busche | 3e4a7bb850 | |
James William Dumay | 2b0ce3fb6d | |
Cliff Meyers | 1316346503 | |
Michael Neale | 789da3dd7d | |
Vivek Pandey | ab145f1620 | |
Vivek Pandey | 915bc218b7 | |
James William Dumay | 6cd2155c75 | |
Cliff Meyers | 0608e5a026 | |
vivek | 849fc8650b | |
Josh McDonald | 47f591995b | |
Vivek Pandey | 6de837223b | |
Vivek Pandey | 24467ad959 | |
Cliff Meyers | 72610dcd0b | |
Tom Fennelly | 624eb0bb0d | |
Cliff Meyers | de288bef83 | |
vivek | 2cbc261e3b | |
Tom Fennelly | 6750113028 | |
Vivek Pandey | 8346066673 | |
Vivek Pandey | baf2bd6e98 | |
vivek | 16a97412fd | |
Cliff Meyers | 3d14e84406 | |
Ivan Meredith | 5686ec1f5f | |
Yoann Dubreuil | 4ad4370873 | |
Tom Fennelly | b959a490bc | |
Adrien Lecharpentier | 0e622a706f | |
Tom Fennelly | 079f73a04d | |
Michael Neale | 97102f3c70 | |
Michael Neale | 9927f75074 | |
Keith Zantow | 628acf9cc6 | |
Thorsten Scherler | 2cf97088a7 | |
Thorsten Scherler | ac7f45a9e7 | |
Thorsten Scherler | 40e04c273d | |
Thorsten Scherler | 434966d4af | |
Thorsten Scherler | 8a6a4f2ecd | |
Yoann Dubreuil | f70e55df71 | |
Tom Fennelly | c21741b563 | |
Thorsten Scherler | 9ccbedff8d | |
Thorsten Scherler | fc8bb5958b | |
Thorsten Scherler | 6329564926 | |
Thorsten Scherler | 633a010e3b | |
Tom Fennelly | c9c1f9b618 | |
paladox | 89d5a2d0be | |
Thorsten Scherler | 5564709a0c | |
Thorsten Scherler | 83f71acb95 | |
Thorsten Scherler | 4d1c750a6b | |
Michael Neale | 4299e97b1c | |
Tom Fennelly | 9ee1fe3d9c | |
Cliff Meyers | ca8ec532cd | |
Tom Fennelly | fee4d570a8 | |
Cliff Meyers | c6cf76ddc1 | |
Yoann Dubreuil | 945198cf34 | |
Thorsten Scherler | b331bdce9c | |
Michael Neale | 2474e0eae6 | |
Michael Neale | 57f77580e6 | |
Thorsten Scherler | 306287176f | |
Thorsten Scherler | 8690946acd | |
Josh McDonald | 452759e5ee | |
Josh McDonald | 21c57745a3 | |
Josh McDonald | c5541b5c40 | |
Josh McDonald | feb23a4def | |
Tom Fennelly | ad04e53ddf | |
Cliff Meyers | 44a460b7e6 | |
Thorsten Scherler | e2905f7eca | |
Josh McDonald | 883e341b51 | |
Michael Neale | 6b4ac8f294 | |
Yoann Dubreuil | 419d13e14f | |
Cliff Meyers | f1475e883a | |
Cliff Meyers | bf64c574dd | |
Cliff Meyers | c1b92d5742 | |
Yoann Dubreuil | 742580e53d | |
Cliff Meyers | d075f7d56f | |
Keith Zantow | 83b81b6ee4 | |
Cliff Meyers | 6207d331c0 | |
Cliff Meyers | aee80261d8 | |
Thorsten Scherler | cd83f83a3f | |
vivek | a78bc763d0 | |
Cliff Meyers | 710652d539 | |
Thorsten Scherler | aa5364591f | |
Ivan Meredith | e8e9d0ddec | |
Thorsten Scherler | 6040ecb4d1 | |
vivek | 3ff03598b4 | |
Cliff Meyers | f675a3b679 | |
Cliff Meyers | ee87f2b3e5 | |
Vivek Pandey | f052917fb8 | |
Vivek Pandey | d47c3c3503 | |
vivek | ae00541d5b | |
Tom Fennelly | 1216a86f66 | |
Vivek Pandey | 9ee76a434f | |
Vivek Pandey | 2af8aab224 | |
Ivan Meredith | 9647650252 | |
vivek | 8cfe6e4962 | |
Tom Fennelly | c078efb2cf | |
Vivek Pandey | ec2f03716e | |
Vivek Pandey | f8fc5aa93b | |
vivek | 01400d785a | |
Keith Zantow | b913d2c0d8 | |
vivek | 8a8db539d2 | |
Keith Zantow | 243914c622 | |
Thorsten Scherler | 4da297d4cc | |
vivek | 44bace7dc4 | |
vivek | 0875637afb | |
vivek | dfff029465 | |
Ivan Meredith | 281e1a2f63 | |
Keith Zantow | f98dc57807 | |
Cliff Meyers | 418269930a | |
Cliff Meyers | 3ddb6804be | |
Vivek Pandey | 16f2da9d22 | |
Vivek Pandey | ecb790d723 | |
Cliff Meyers | e781d8d6fd | |
Thorsten Scherler | c1a354010b | |
Vivek Pandey | 8bcbc7273a | |
Vivek Pandey | 3f9cea5c4c | |
vivek | 09700bc407 | |
vivek | ab49ee788e | |
vivek | 0d64ef6bd8 | |
Cliff Meyers | 3072d3cc70 | |
Cliff Meyers | af9d03d3b3 | |
Thorsten Scherler | 4e2cbc28d8 | |
Vivek Pandey | fdd874477e | |
Vivek Pandey | 36f5918e7b | |
Cliff Meyers | 81f3dee521 | |
Cliff Meyers | fbd1ebdee2 | |
Thorsten Scherler | 7bfe44768a | |
Thorsten Scherler | ef04196dd3 | |
Thorsten Scherler | 3826bd9b9f | |
vivek | c713cf4f15 | |
Ivan Meredith | 52b8b14d4f | |
vivek | a0d9e5a0b2 | |
Tom Fennelly | 1d0ce63b2c | |
Ivan Meredith | 197517d34e | |
Ivan Meredith | b6d0840772 | |
Ivan Meredith | 88cbacd967 | |
Keith Zantow | 898c368656 | |
James William Dumay | 229796da80 | |
Ivan Meredith | 7b58c5f582 | |
Ivan Meredith | 575cd6af01 | |
Ivan Meredith | d4f5e4bb87 | |
Ivan Meredith | 1013b07c4b | |
Ivan Meredith | 136e512d73 | |
vivek | 206af8bfa5 | |
Ivan Meredith | 8fa02c4ffa | |
Keith Zantow | c6f4b9b085 | |
Ivan Meredith | f391a84f0a | |
Ivan Meredith | fd0b8cfa2d | |
Vivek Pandey | 94f08c2dd5 | |
Vivek Pandey | dff4d45443 | |
Vivek Pandey | 2f2296f433 | |
Tom Fennelly | bff98212b7 | |
vivek | 3194e75943 | |
Ivan Meredith | f1e8092f02 | |
Ivan Meredith | bd06cc34ec | |
Ivan Meredith | 986fd51fc6 | |
Vivek Pandey | 91c3169280 | |
Vivek Pandey | 815501c0d0 | |
James Dumay | b358bb3e5d | |
Keith Zantow | 9ce99c7501 | |
Keith Zantow | 9617f7f60e | |
Keith Zantow | f8efce4ab1 | |
Tom Fennelly | c40c6277c6 | |
tfennelly | fd8eb0afae | |
tfennelly | 3df6c9de7a | |
Keith Zantow | 092fc6d588 | |
Thorsten Scherler | 9a8d1a77f7 | |
Ivan Meredith | 2082074c91 | |
Keith Zantow | a230a129a5 | |
Keith Zantow | b1b60a3b6a | |
James William Dumay | e50a21bd81 | |
Cliff Meyers | 3ef99dd7e8 | |
Thorsten Scherler | 0f6f79bb0e | |
Thorsten Scherler | 68cd8cd122 | |
Tom Fennelly | 9eaadfbb18 | |
Ivan Meredith | 17fa7af60c | |
Ivan Meredith | 2e6d111351 | |
Cliff Meyers | 17e620effd | |
Ivan Meredith | 857364479d | |
Ivan Meredith | e95bc044dd | |
vivek | 9c6ccbdf63 | |
Ivan Meredith | 9b56aa3e8a | |
Ivan Meredith | 59b9eafd03 | |
Ivan Meredith | e42684b368 | |
Ivan Meredith | 21ae9ecbff | |
Ivan Meredith | 8fa663e685 | |
Vivek Pandey | af1f628489 | |
Tom Fennelly | d91d5d7c19 | |
Ivan Meredith | 0374e0ecb4 | |
Ivan Meredith | 4f9a2ba12d | |
Ivan Meredith | 1d730ff5c9 | |
Ivan Meredith | 5acc432c50 | |
Ivan Meredith | 3ee681ec72 | |
Thorsten Scherler | d3f95ceb4f | |
Thorsten Scherler | 67cc62155e | |
Thorsten Scherler | 9ac41d292a | |
Thorsten Scherler | c9d420f83d | |
Thorsten Scherler | 51a340de63 | |
Ivan Meredith | b9464a874c | |
Ivan Meredith | 9a15765a12 | |
Ivan Meredith | 4dc626053d | |
Ivan Meredith | dca9a345b2 | |
Ivan Meredith | 4103fcfb98 | |
Cliff Meyers | f03f20f091 | |
Thorsten Scherler | 3230730393 | |
Ivan Meredith | f864f701b4 | |
Ivan Meredith | 3001e11d53 | |
Vivek Pandey | dc3e155cab | |
Vivek Pandey | 8ca97f2820 | |
Thorsten Scherler | 5132008c19 | |
Ivan Meredith | 280176cda1 | |
Ivan Meredith | d8c4c1b1a3 | |
Ivan Meredith | aa4b8bd1df | |
Ivan Meredith | 6792a9e93f | |
Ivan Meredith | efce1a99d3 | |
Ivan Meredith | 853c170d3c | |
Vivek Pandey | 49da081db2 | |
Vivek Pandey | 25a0e60be5 | |
Vivek Pandey | 47a9f5fe3d | |
Vivek Pandey | 014901a89f | |
Thorsten Scherler | 08e10f81ab | |
Thorsten Scherler | 3f5242b7a2 | |
Thorsten Scherler | e663ce5812 | |
vivek | 05537ffadc | |
vivek | 40c56c0801 | |
Cliff Meyers | e58f6ce693 | |
Thorsten Scherler | b6c9d2b0ba | |
Thorsten Scherler | 1620591944 | |
vivek | 8f0e2b2315 | |
vivek | 00521c87ca | |
Cliff Meyers | 9f3602408d | |
Cliff Meyers | ee7a567c52 | |
Cliff Meyers | df13370eb8 | |
Cliff Meyers | c04365c918 | |
Cliff Meyers | c876c6e346 | |
Cliff Meyers | c9c5b41d9d | |
Cliff Meyers | a40726470c | |
Vivek Pandey | ee2906d315 | |
Vivek Pandey | 521a495317 | |
Cliff Meyers | 458ec7930f | |
Cliff Meyers | 96a449e37c | |
Cliff Meyers | 49c554f70d | |
Cliff Meyers | 6566ada191 | |
Cliff Meyers | d42b6ebe30 | |
Cliff Meyers | aa457e708a | |
Cliff Meyers | a7c9d93f87 | |
Cliff Meyers | d5af8f974b | |
vivek | 1c8a6f6581 | |
Cliff Meyers | bfabaa1e6a | |
Ivan Meredith | 128267428a | |
Cliff Meyers | dfe2f86744 | |
Cliff Meyers | bc535774b7 | |
Ivan Meredith | 5cf1fab9ad | |
Tom Fennelly | 1fe80545b7 | |
Cliff Meyers | 3a3da80a08 | |
Cliff Meyers | 965a8bc923 | |
Cliff Meyers | ddac8e7c2e | |
Cliff Meyers | 243fcbc0e1 | |
Cliff Meyers | 9ef2b591c5 | |
Cliff Meyers | 001f9ff20c | |
Cliff Meyers | 7da4051c1f | |
Cliff Meyers | 08d182fdf4 | |
Ivan Meredith | 05ac87f86f | |
R. Tyler Croy | 74826326fb | |
Cliff Meyers | 895177c9fb | |
Cliff Meyers | de36bfab87 | |
Cliff Meyers | 93a0968449 | |
Cliff Meyers | 820d9d7c9f | |
Cliff Meyers | 7e1a81e928 | |
Cliff Meyers | fcc0a421a4 | |
Cliff Meyers | d94f197878 | |
Cliff Meyers | fb22a820d5 | |
Cliff Meyers | 3e00e91d42 | |
Cliff Meyers | d087530e52 |
|
@ -1,8 +1,3 @@
|
|||
*
|
||||
!blueocean-commons/target/blueocean-commons.hpi
|
||||
!blueocean-dashboard/target/blueocean-dashboard.hpi
|
||||
!blueocean-plugin/target/blueocean.hpi
|
||||
!blueocean-rest/target/blueocean-rest.hpi
|
||||
!blueocean-rest-impl/target/blueocean-rest-impl.hpi
|
||||
!blueocean-web/target/blueocean-web.hpi
|
||||
!docker-demo/
|
||||
!blueocean/target/plugins
|
||||
!docker
|
||||
|
|
|
@ -14,5 +14,8 @@ npm-debug.log
|
|||
**/.settings/
|
||||
**/.project
|
||||
**/.classpath
|
||||
blueocean-web/src/main/webapp/
|
||||
blueocean-web/src/main/webapp/assets
|
||||
.build_container
|
||||
.watch_trigger
|
||||
.vscode
|
||||
jsconfig.json
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Blue Ocean is a Jenkins project that aspires to be well known and loved by Jenkins users.
|
||||
Thus, similar contributing guidelines apply as to Jenkins itself.
|
||||
|
||||
For information on contributing to Jenkins, check out the https://wiki.jenkins-ci.org/display/JENKINS/contributing and https://wiki.jenkins-ci.org/display/JENKINS/Extend+Jenkins wiki pages over at the official https://wiki.jenkins-ci.org. They will help you get started with contributing to Jenkins.
|
||||
For information on contributing to Jenkins, check out the https://wiki.jenkins-ci.org/display/JENKINS/Beginners+Guide+to+Contributing and https://wiki.jenkins-ci.org/display/JENKINS/Extend+Jenkins wiki pages over at the official https://wiki.jenkins-ci.org. They will help you get started with contributing to Jenkins.
|
||||
|
||||
|
||||
## Changes and pull requests
|
||||
|
@ -22,7 +22,9 @@ Once the PR is no longer a __work-in-progress__, is building fine (according to
|
|||
|
||||
Avoid "bike shed" discussions about styles or whitespace unless it really impacts the changeset. The contributor can be encouraged to apply editor automation in future (if it is available).
|
||||
|
||||
Squashing commits: if there are messy intermediate commits it is nice to squash things for the reviewer (but not mandatory). Always think about how to make it quick and easy for a reviewer (perhaps with more smaller PR's if needed).
|
||||
# Merging
|
||||
|
||||
All commits should be squashed before being merged to `master` to preserve a sane history. The Merge button on the PR screen has been configured to do this automatically.
|
||||
|
||||
# Code Style
|
||||
|
||||
|
|
43
Dockerfile
43
Dockerfile
|
@ -1,33 +1,26 @@
|
|||
FROM jenkinsci/jenkins:latest
|
||||
#
|
||||
# Before building this Dockerfile, BlueOcean needs to be built locally using Maven
|
||||
# You can build everything needed and this Dockerfile by invoking `bin/build-in-docker.sh -m`
|
||||
#
|
||||
|
||||
COPY blueocean-commons/target/blueocean-commons.hpi /usr/share/jenkins/ref/plugins/
|
||||
COPY blueocean-dashboard/target/blueocean-dashboard.hpi /usr/share/jenkins/ref/plugins/
|
||||
COPY blueocean-plugin/target/blueocean.hpi /usr/share/jenkins/ref/plugins/blueocean-plugin.hpi
|
||||
COPY blueocean-rest/target/blueocean-rest.hpi /usr/share/jenkins/ref/plugins/
|
||||
COPY blueocean-rest-impl/target/blueocean-rest-impl.hpi /usr/share/jenkins/ref/plugins/
|
||||
COPY blueocean-web/target/blueocean-web.hpi /usr/share/jenkins/ref/plugins/
|
||||
# Should be kept in sync with jenkins.properties of pom.xml
|
||||
# Patch version is not to be considered, we prefer to base the image off the latest LTS of the line
|
||||
# and keep the dependency on the baseline in pom.xml
|
||||
FROM jenkins:2.7.4
|
||||
|
||||
USER root
|
||||
|
||||
RUN cd /usr/share/jenkins/ref/plugins/; \
|
||||
install-plugins.sh blueocean-commons \
|
||||
blueocean-dashboard \
|
||||
blueocean-plugin \
|
||||
blueocean-rest \
|
||||
blueocean-web \
|
||||
workflow-aggregator \
|
||||
docker-workflow \
|
||||
pipeline-utility-steps \
|
||||
pipeline-stage-view \
|
||||
git \
|
||||
antisamy-markup-formatter \
|
||||
matrix-auth # for security, you know
|
||||
COPY blueocean/target/plugins /usr/share/jenkins/ref/plugins/
|
||||
|
||||
# Force use of latest blueocean plugin, until this one is published and users can rely on update center for updates
|
||||
RUN for f in /usr/share/jenkins/ref/plugins/blueocean-*.hpi; do mv "$f" "$f.override"; done
|
||||
RUN for f in /usr/share/jenkins/ref/plugins/*.hpi; do mv "$f" "${f%%hpi}jpi"; done
|
||||
RUN install-plugins.sh antisamy-markup-formatter matrix-auth # for security, you know
|
||||
|
||||
# See JENKINS-34035 - disable upgrade wizard
|
||||
RUN echo -n 2.0 > /usr/share/jenkins/ref/jenkins.install.UpgradeWizard.state && \
|
||||
echo -n 2.0 > /usr/share/jenkins/ref/jenkins.install.InstallUtil.lastExecVersion
|
||||
# Force use of locally built blueocean plugin
|
||||
RUN for f in /usr/share/jenkins/ref/plugins/blueocean-*.jpi; do mv "$f" "$f.override"; done
|
||||
|
||||
# let scripts customize the reference Jenkins folder. Used in bin/build-in-docker to inject the git build data
|
||||
COPY docker/ref /usr/share/jenkins/ref
|
||||
|
||||
RUN npm install -g npm@3.10.9
|
||||
|
||||
USER jenkins
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
FROM ubuntu:16.04
|
||||
|
||||
ENV MAVEN_VERSION 3.3.3
|
||||
ENV NODE_VERSION 6.4.0
|
||||
|
||||
USER root
|
||||
|
||||
RUN apt-get update
|
||||
|
||||
#========================
|
||||
# Miscellaneous packages
|
||||
#========================
|
||||
RUN apt-get update -qqy \
|
||||
&& apt-get -qqy --no-install-recommends install \
|
||||
sudo \
|
||||
openjdk-8-jdk \
|
||||
tar \
|
||||
zip xz-utils \
|
||||
curl wget \
|
||||
git \
|
||||
build-essential \
|
||||
python \
|
||||
iputils-ping \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& sed -i 's/securerandom\.source=file:\/dev\/random/securerandom\.source=file:\/dev\/urandom/' ./usr/lib/jvm/java-8-openjdk-amd64/jre/lib/security/java.security
|
||||
|
||||
#==========
|
||||
# Maven
|
||||
#==========
|
||||
RUN curl -fsSL http://archive.apache.org/dist/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz | tar xzf - -C /usr/share \
|
||||
&& mv /usr/share/apache-maven-$MAVEN_VERSION /usr/share/maven \
|
||||
&& ln -s /usr/share/maven/bin/mvn /usr/bin/mvn
|
||||
ENV MAVEN_HOME /usr/share/maven
|
||||
|
||||
|
||||
#===============
|
||||
# Node and NPM
|
||||
#===============
|
||||
RUN wget --no-verbose https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz -O /opt/nodejs.tar.xz
|
||||
RUN tar -C /usr/local --strip-components 1 -xJf /opt/nodejs.tar.xz
|
||||
RUN mkdir /.npm && chmod 777 /.npm
|
||||
|
||||
#=============================================
|
||||
# Misc packages needed by the ATH
|
||||
#=============================================
|
||||
RUN apt-get update -qqy \
|
||||
&& apt-get -qqy --no-install-recommends install \
|
||||
libxml2-utils \
|
||||
libssl-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
#========================================
|
||||
# Add normal user with passwordless sudo
|
||||
#========================================
|
||||
RUN sudo useradd bouser --shell /bin/bash --create-home \
|
||||
&& sudo usermod -a -G sudo bouser \
|
||||
&& echo 'ALL ALL = (ALL) NOPASSWD: ALL' >> /etc/sudoers \
|
||||
&& echo 'bouser:secret' | chpasswd
|
||||
|
||||
USER bouser
|
||||
WORKDIR /home/bouser
|
||||
|
||||
#========================================
|
||||
# Configure the local git user.
|
||||
#========================================
|
||||
RUN git config --global user.name "John Doe"
|
||||
RUN git config --global user.email johndoe@example.com
|
|
@ -1,14 +1,28 @@
|
|||
#!groovy
|
||||
|
||||
// only 20 builds
|
||||
properties([buildDiscarder(logRotator(artifactNumToKeepStr: '20', numToKeepStr: '20'))])
|
||||
|
||||
node {
|
||||
|
||||
deleteDir()
|
||||
checkout scm
|
||||
sh 'docker build -t blueocean_build_env - < Dockerfile.build'
|
||||
|
||||
docker.image('cloudbees/java-build-tools').inside {
|
||||
configFileProvider([configFile(fileId: 'blueocean-maven-settings', targetLocation: 'settings.xml')]) {
|
||||
|
||||
docker.image('blueocean_build_env').inside {
|
||||
withEnv(['GIT_COMMITTER_EMAIL=me@hatescake.com','GIT_COMMITTER_NAME=Hates','GIT_AUTHOR_NAME=Cake','GIT_AUTHOR_EMAIL=hates@cake.com']) {
|
||||
try {
|
||||
sh "mvn clean install -B -DcleanNode -Dmaven.test.failure.ignore"
|
||||
sh "node checkdeps.js"
|
||||
sh 'npm --prefix ./blueocean-core-js install'
|
||||
sh 'npm --prefix ./blueocean-core-js run gulp'
|
||||
sh "mvn clean install -B -DcleanNode -Dmaven.test.failure.ignore -s settings.xml -Dmaven.artifact.threads=30"
|
||||
sh "node ./bin/checkdeps.js"
|
||||
sh "node ./bin/checkshrinkwrap.js"
|
||||
step([$class: 'JUnitResultArchiver', testResults: '**/target/surefire-reports/TEST-*.xml'])
|
||||
step([$class: 'ArtifactArchiver', artifacts: '*/target/*.hpi'])
|
||||
|
||||
triggerATH();
|
||||
} catch(err) {
|
||||
currentBuild.result = "FAILURE"
|
||||
} finally {
|
||||
|
@ -17,6 +31,31 @@ node {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def triggerATH() {
|
||||
// Assemble and archive the HPI plugins that the ATH should use.
|
||||
// The ATH build can copy this artifact and use it, saving the time it
|
||||
// would otherwise spend building and assembling again.
|
||||
sh 'cd blueocean && tar -czvf target/ath-plugins.tar.gz target/plugins'
|
||||
archiveArtifacts artifacts: 'blueocean/target/ath-plugins.tar.gz'
|
||||
|
||||
// Trigger the ATH, but don't wait for it.
|
||||
try {
|
||||
echo "Will attempt to run the ATH with the same branch name i.e. '${env.BRANCH_NAME}'."
|
||||
build(job: "ATH-Jenkinsfile/${env.BRANCH_NAME}",
|
||||
parameters: [string(name: 'BLUEOCEAN_BRANCH_NAME', value: "${env.BRANCH_NAME}"),
|
||||
string(name: 'BUILD_NUM', value: "${env.BUILD_NUMBER}")],
|
||||
wait: false)
|
||||
} catch (e1) {
|
||||
echo "Failed to run the ATH with the same branch name i.e. '${env.BRANCH_NAME}'. Will try running the ATH 'master' branch."
|
||||
build(job: "ATH-Jenkinsfile/master",
|
||||
parameters: [string(name: 'BLUEOCEAN_BRANCH_NAME', value: "${env.BRANCH_NAME}"),
|
||||
string(name: 'BUILD_NUM', value: "${env.BUILD_NUMBER}")],
|
||||
wait: false)
|
||||
}
|
||||
}
|
||||
|
||||
def sendhipchat() {
|
||||
|
|
|
@ -5,13 +5,13 @@ See [JENKINS-XXXXX](https://issues.jenkins-ci.org/browse/JENKINS-XXXXX).
|
|||
# Submitter checklist
|
||||
- [ ] Link to JIRA ticket in description, if appropriate.
|
||||
- [ ] Change is code complete and matches issue description
|
||||
- [ ] Apppropriate unit or acceptance tests or explaination to why this change has no tests
|
||||
- [ ] Appropriate unit or acceptance tests or explanation to why this change has no tests
|
||||
- [ ] Reviewer's manual test instructions provided in PR description. See Reviewer's first task below.
|
||||
- [ ] Ran Acceptance Test Harness against PR changes.
|
||||
|
||||
# Reviewer checklist
|
||||
- [ ] Run the changes and verified the change matches the issue description
|
||||
- [ ] Reviewed the code
|
||||
- [ ] Verified that the appropriate tests have been written or valid explaination given
|
||||
- [ ] Verified that the appropriate tests have been written or valid explanation given
|
||||
|
||||
@jenkinsci/code-reviewers @reviewbybees
|
||||
@reviewbybees
|
||||
|
|
146
README.md
146
README.md
|
@ -1,51 +1,65 @@
|
|||
Blue Ocean is the next generation user experience for Jenkins.
|
||||
# Blue Ocean
|
||||
Blue Ocean is the next generation user experience for Jenkins. You can learn more about its features and roadmap on the [Blue Ocean project website](https://jenkins.io/projects/blueocean/).
|
||||
|
||||
It is a multi-module maven project with a few Jenkins plugins.
|
||||
Join the community and [![Gitter](https://badges.gitter.im/jenkinsci/blueocean-plugin.svg)](https://gitter.im/jenkinsci/blueocean-plugin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
Read it:
|
||||
We would also like to thank [![Rollbar](https://d26gfdfi90p7cf.cloudfront.net/rollbar-badge.144534.o.png)](http://rollbar.com) for providing us error reporting.
|
||||
|
||||
# Get Blue Ocean
|
||||
|
||||
Blue Ocean is [available from the Jenkins update center](https://jenkins.io/projects/blueocean/#use-the-beta) for Jenkins 2.7.1 and above.
|
||||
|
||||
# Reporting bugs and feature requests
|
||||
|
||||
We use the [Jenkins JIRA](https://issues.jenkins-ci.org/) to log all bugs and feature requests. Create a [new account](https://accounts.jenkins.io/), [browse to JIRA](https://issues.jenkins-ci.org/) and login with your account then create a new issue with the component `blueocean-plugin`.
|
||||
|
||||
# For Developers
|
||||
It is a multi-module maven project made up of a few Jenkins plugins. There is an aggregator plugin in the "blueocean" module.
|
||||
|
||||
CONTRIBUTIONS ALWAYS WELCOME NO MATTER HOW BIG OR SMALL.
|
||||
|
||||
Some background reading:
|
||||
https://jenkins.io/blog/2016/05/26/introducing-blue-ocean/
|
||||
|
||||
This is in the main Update Center for Jenkins. Install the plugin called "BlueOcean beta" (and let it install its dependencies). The instructions here are for contributors to Blue Ocean and the morbidly curious. Expect refactoring.
|
||||
|
||||
|
||||
|
||||
![Pirate logo, because it's ocean and stuff](logo-yarrr.png)
|
||||
![Pirate logo, because it's ocean and stuff](docu/pix/logo-yarrr.png)
|
||||
Yarr...
|
||||
|
||||
# Modules of note
|
||||
## blueocean-dashboard
|
||||
## Modules of note
|
||||
|
||||
Blue Ocean Dashboard plugin. Currently contains the bulk of the Blue Ocean user interface. This is mostly client side JavaScript built with ES6 and React.
|
||||
A quick tour of some of the modules (not all). Consult README.md in respective modules for more info.
|
||||
|
||||
## blueocean-plugin
|
||||
### blueocean-dashboard
|
||||
|
||||
Acts as an aggregator plugin, making it an easy place from which to run Blue Ocean via `hpi:run`.
|
||||
Blue Ocean Dashboard plugin. Currently contains a lot of the core of the Blue Ocean user interface and extension points. This is mostly client side JavaScript built with ES6 and React.
|
||||
|
||||
__NOTE__: As already stated, this plugin is likely to be refactored in the near future.
|
||||
### blueocean
|
||||
|
||||
An aggregator plugin, making it an easy place from which to run Blue Ocean via `hpi:run`.
|
||||
|
||||
### blueocean-rest
|
||||
|
||||
Java interfaces and classes that specify the definition of the REST API that blueocean uses. See the README.md within this module for detail on this api.
|
||||
|
||||
### blueocean-rest-impl
|
||||
|
||||
Provides the default implementation of the core REST Apis defined in the `blueocean-rest` plugin. It comes with only freestyle job support.
|
||||
|
||||
|
||||
## blueocean-rest
|
||||
|
||||
Java interfaces and classes that specify the definition of the REST API. See the README within this module for more information.
|
||||
|
||||
## blueocean-rest-impl
|
||||
|
||||
Provides the default implementation of the core REST Apis defined in the `blueocean-rest` plugin. It comes with only free style job support.
|
||||
|
||||
|
||||
## blueocean-pipeline-api-impl
|
||||
### blueocean-pipeline-api-impl
|
||||
|
||||
Provides implementation of Pipeline apis for Jenkins pipeline and multi-branch job types support
|
||||
|
||||
### blueocean-web
|
||||
|
||||
## blueocean-web
|
||||
Core Web infrastructure that bootstraps BlueOcean UI and integrates REST API core blueocean-rest, and serves up the core javascript libraries.
|
||||
|
||||
Core Web infrastructure that bootstraps BlueOcean UI and integrates REST API core blueocean-rest.
|
||||
|
||||
## blueocean-analytics-tools
|
||||
|
||||
Plugin to inject analytics tools as HTML header in blueocean UI.
|
||||
|
||||
# Building and running
|
||||
## Building and running
|
||||
|
||||
At a minimum you will need JVM and Maven installed, if you are doing active JavaScript development, installing NodeJS is a good idea too.
|
||||
|
||||
## Build everything (from root directory)
|
||||
Builds all maven modules (run this the first time you check things out, at least)
|
||||
|
@ -60,28 +74,28 @@ For now, you'll need to skip the tests if __building on Windows__, so be sure to
|
|||
$ mvn clean install -DskipTests
|
||||
```
|
||||
|
||||
## Running Blue Ocean
|
||||
### Running Blue Ocean
|
||||
|
||||
```
|
||||
$ mvn -f blueocean-plugin/pom.xml hpi:run
|
||||
$ mvn -f blueocean/pom.xml hpi:run
|
||||
```
|
||||
|
||||
Then open http://localhost:8080/jenkins/blue to start using Blue Ocean.
|
||||
|
||||
The Jenkins Classic UI exists side-by-side at its usual place at http://localhost:8080/jenkins.
|
||||
|
||||
# Browser compatibility
|
||||
## Browser compatibility
|
||||
|
||||
The obviously goal is for Blue Ocean to be runnable on all browsers on all platforms. We're not there yet, but getting
|
||||
closer. The ultimate goal will be to have browser support in line with the [Jenkins Browser Compatibility Matrix](https://wiki.jenkins-ci.org/display/JENKINS/Browser+Compatibility+Matrix).
|
||||
|
||||
List of browsers where we know Blue Ocean is not yet runnable:
|
||||
|
||||
* Internet Explorer (all versions) on Windows
|
||||
* Internet Explorer < 11 on Windows (the aim is to keep IE 11 working, but help is needed to maintain a Windows test environment in the pipeline)
|
||||
|
||||
* AmigaOS
|
||||
|
||||
|
||||
# Developing
|
||||
## Developing
|
||||
|
||||
Follow the steps above for getting it running first.
|
||||
|
||||
|
@ -90,18 +104,68 @@ Look in following README's for:
|
|||
* ``blueocean-rest`` for how to navigate the rest api.
|
||||
* ``blueocean-rest-impl`` for more details on how to actively develop this plugin for backend codebases.
|
||||
|
||||
### Tools needed
|
||||
|
||||
### Building plugins for Blue Ocean
|
||||
|
||||
Blue Ocean plugins use the same plugin mechanism as Jenkins for distribution and installation, but involve a lot more Javascript if they have GUI elements.
|
||||
|
||||
The best way to get started is to look at the tutorial and Yeoman starter project here:
|
||||
https://www.npmjs.com/package/generator-blueocean-usain
|
||||
The usual plugin guide also applies for Jenkins: https://wiki.jenkins-ci.org/display/JENKINS/Plugin+tutorial#Plugintutorial-CreatingaNewPlugin
|
||||
|
||||
Ask for help in #jenkins-ci or on the mailing list if you are working on a plugin.
|
||||
|
||||
|
||||
#### Tools needed
|
||||
|
||||
*Maven* is used for most building. The project is configured to grab all the tools you need from the JavaScript ecosystem to get started.
|
||||
|
||||
If you are working on the Javascript, you will need node and gulp installed.
|
||||
|
||||
If you are working on the Javascript, you will need node installed, look at the version in the pom.xml for the minimum version required.
|
||||
|
||||
__NOTE__: look in the README.md of the respective modules for more detailed dev docs.
|
||||
|
||||
#### NPM and shrinkwrap
|
||||
|
||||
- Ensure your npm is 3.10.8+ as this release fixes some important bugs with shrinkwrap, notably #11735 in [notes](https://github.com/npm/npm/releases/tag/v3.10.8)
|
||||
- Don't edit package.json directly; use npm install to ensure that both package.json and npm-shrinkwrap.json are updated.
|
||||
- To add or update a dependency:
|
||||
- `npm install packageName@3.2.1 -S -E`
|
||||
- To add or update a devDependency:
|
||||
- `npm install packageName@3.2.1 -D -E`
|
||||
- If you are handling a merge conflict in package.json, resolve the conflict in the file as normal. Then use
|
||||
the appropriate command to update each conflicting dependency to ensure shrinkwrap is updated.
|
||||
- To remove a dependency:
|
||||
- `npm uninstall packageName -S`
|
||||
- To remove a devDependency:
|
||||
- `npm uninstall packageName -D`
|
||||
- If you ever need to create a shrinkwrap for the first time, use `npm shrinkwrap --dev` to ensure devDependencies are
|
||||
included in the shrinkwrap.
|
||||
|
||||
Full docs on [npm shrinkwrap](https://docs.npmjs.com/cli/shrinkwrap)
|
||||
Information on [building with shrinkwrap](https://docs.npmjs.com/cli/shrinkwrap#building-shrinkwrapped-packages)
|
||||
|
||||
In case you want to update your dependencies with something like ```npm-check-updates``` make sure you follow the simple steps:
|
||||
|
||||
```
|
||||
ncu -a
|
||||
rm -rf node_modules npm-shrinkwrap.json
|
||||
npm i
|
||||
npm shrinkwrap --dev
|
||||
```
|
||||
|
||||
|
||||
# Debug and live reload with IntelliJ
|
||||
## Contributing - help wanted
|
||||
|
||||
### i18n - Sprechen Sie Deutsch?
|
||||
|
||||
We have full i18n support in our plugins. Please read the [i18n documentation](./docu/I18N.md) on how you can provide new translations and how to work with i18n.
|
||||
|
||||
### contributing guidelines
|
||||
|
||||
Want to get involve with blueocean? See our [contributing guidelines](./CONTRIBUTING.md) for more informations.
|
||||
|
||||
|
||||
## Debug and live reload with IntelliJ
|
||||
Automatically deploys changes to an instance of blueocean that is run with hpi:run.
|
||||
|
||||
1. Enable class reloading: Preferences > Build, Execution, Deployment > Debugger > HotSwap
|
||||
|
@ -114,12 +178,14 @@ Automatically deploys changes to an instance of blueocean that is run with hpi:r
|
|||
* Runner > VM Options: `-Dblueocean.config.file=../app.properties`
|
||||
3. Debug new configuration, and after compilation the class file will be reloaded
|
||||
|
||||
# Help
|
||||
## Help
|
||||
|
||||
Need help?
|
||||
|
||||
You can chat to folks on #jenkins-ux on freenode (IRC). You can also email the jenkins-dev email list (google group: https://groups.google.com/forum/#!forum/jenkinsci-dev) - but ensure you use the prefix [Blue Ocean] in your subject line when posting.
|
||||
|
||||
# Presentations
|
||||
## Presentations
|
||||
|
||||
Advanced front end development with react, redux and stuff by @scherler: https://docs.google.com/presentation/d/1dbaYTIGjGT9xX1JnWnaqjMumq94M9nGwljfMQaVtUFc/edit?usp=sharing
|
||||
|
||||
Watch @i386 and @jenkinsci on Twitter for frequent updates and news.
|
||||
|
|
|
@ -1,9 +1,23 @@
|
|||
#!/usr/bin/env bash
|
||||
set -eu -o pipefail
|
||||
|
||||
HERE="$(cd -P "$( dirname "${BASH_SOURCE[0]}" )" && pwd)"
|
||||
PROJECT_ROOT="$(cd -P "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd)"
|
||||
|
||||
setup_nice_output() {
|
||||
|
||||
bold=""
|
||||
underline=""
|
||||
standout=""
|
||||
normal=""
|
||||
black=""
|
||||
red=""
|
||||
green=""
|
||||
yellow=""
|
||||
blue=""
|
||||
magenta=""
|
||||
cyan=""
|
||||
white=""
|
||||
|
||||
# check if stdout is a terminal...
|
||||
if [ -t 1 ]; then
|
||||
|
||||
|
@ -30,13 +44,13 @@ setup_nice_output() {
|
|||
new_build_container() {
|
||||
local build_image=$1; shift
|
||||
|
||||
build_container=$(docker create -i -v "$HERE":/build -w /build "$build_image" /bin/cat)
|
||||
echo "$build_container" > "$HERE/.build_container"
|
||||
build_container=$(docker create -i -v "$PROJECT_ROOT":/build -w /build "$build_image" /bin/cat)
|
||||
echo "$build_container" > "$PROJECT_ROOT/.build_container"
|
||||
}
|
||||
|
||||
delete_build_container() {
|
||||
docker rm "$build_container"
|
||||
rm "$HERE/.build_container"
|
||||
rm "$PROJECT_ROOT/.build_container"
|
||||
}
|
||||
|
||||
stop_build_container() {
|
||||
|
@ -53,8 +67,8 @@ stop_trap() {
|
|||
|
||||
prepare_build_container() {
|
||||
local build_image=$1; shift
|
||||
if [[ -f $HERE/.build_container ]]; then
|
||||
read -r build_container < "$HERE/.build_container"
|
||||
if [[ -f $PROJECT_ROOT/.build_container ]]; then
|
||||
read -r build_container < "$PROJECT_ROOT/.build_container"
|
||||
else
|
||||
new_build_container "$build_image"
|
||||
return
|
||||
|
@ -64,7 +78,7 @@ prepare_build_container() {
|
|||
echo "${yellow}=> ${normal}Removing old build container ${build_container}"
|
||||
docker kill "$build_container" || true
|
||||
docker rm "$build_container" || true
|
||||
rm "$HERE/.build_container"
|
||||
rm "$PROJECT_ROOT/.build_container"
|
||||
else
|
||||
local state; state=$(docker inspect --format="{{ .State.Status }}" "$build_container")
|
||||
if [[ $? -ne 0 || "$state" != "exited" ]]; then
|
||||
|
@ -98,21 +112,33 @@ build_inside() {
|
|||
stop_build_container
|
||||
}
|
||||
|
||||
build-git-description() {
|
||||
local head="$(git rev-parse --verify HEAD)"
|
||||
echo "BlueOcean plugins built from commit <a href=\"https://github.com/jenkinsci/blueocean-plugin/commit/${head}\">${head}</a>"
|
||||
local pr="$(git show-ref | sed -n "s|^$head refs/remotes/.*/pr/\(.*\)$|\1|p")"
|
||||
if [[ ! -z $pr ]]; then
|
||||
echo ", <a href=\"https://github.com/jenkinsci/blueocean-plugin/pull/${pr}\">Pull Request ${pr}</a><br>"
|
||||
fi
|
||||
}
|
||||
|
||||
make_image() {
|
||||
echo "${yellow}=> ${normal}Building BlueOcean docker image ${tag_name}"
|
||||
(cd "$HERE" && docker build -t "$tag_name" . )
|
||||
echo "${yellow}=> ${normal}Building BlueOcean docker development image ${tag_name}"
|
||||
(cd "$PROJECT_ROOT" && docker build -t "$tag_name" . )
|
||||
}
|
||||
|
||||
build_commands="mvn clean install -B -DcleanNode -Dmaven.test.failure.ignore"
|
||||
tag_name=blueocean-local
|
||||
tag_name="blueocean-dev:local"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
usage: $(basename $0) [-c|--clean] [-m|--make-image[=tag_name]] [-h|--help] [BUILD_COMMAND]
|
||||
usage: $(basename $0) [-c|--clean] [-m|--make-image[=tag_name]] [-g|--git-data] [-h|--help] [BUILD_COMMAND]
|
||||
|
||||
Build BlueOcean plugin suite locally like it would be in Jenkins, by isolating the build
|
||||
inside a Docker container. Requires a local Docker daemon to work.
|
||||
Can also create a BlueOcean docker image if '-m' is passed.
|
||||
|
||||
Create a BlueOcean docker dev image with Dockerfile if '-m' is passed and inject git revision data
|
||||
to it if '-g' is passed.
|
||||
|
||||
In order to speed up builds, the build container is kept between builds in order to keep
|
||||
Maven / NPM caches. It can be cleaned up with '-c' option.
|
||||
|
||||
|
@ -125,6 +151,7 @@ EOF
|
|||
|
||||
clean=false
|
||||
make_image=false
|
||||
git_data=false
|
||||
|
||||
for i in "$@"; do
|
||||
case $i in
|
||||
|
@ -144,6 +171,10 @@ for i in "$@"; do
|
|||
make_image=true
|
||||
shift # past argument=value
|
||||
;;
|
||||
-g|--git-data)
|
||||
git_data=true
|
||||
shift # past argument=value
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
|
@ -154,6 +185,13 @@ if [[ $# -ne 0 ]]; then build_commands="$*"; fi
|
|||
|
||||
setup_nice_output
|
||||
build_inside "cloudbees/java-build-tools"
|
||||
if [[ "$git_data" = true ]]; then
|
||||
mkdir -p "$PROJECT_ROOT/docker/ref/init.groovy.d"
|
||||
cat > "$PROJECT_ROOT/docker/ref/init.groovy.d/build_data.groovy" <<EOF
|
||||
jenkins.model.Jenkins.instance.setSystemMessage('''$(build-git-description)''')
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [[ "$make_image" = true ]]; then
|
||||
make_image
|
||||
fi
|
|
@ -12,7 +12,10 @@
|
|||
|
||||
Any conflicting PROD dependencies will be printed on STDERR, and it will exit(1)
|
||||
|
||||
If no conflicts, or only PEER/DEV conflicts, normal exit(0)
|
||||
Any conflicting DEV or PEER dependencies for packages in @jenkins-cd/ npm group will be printed on STDERR,
|
||||
and it will exit(1)
|
||||
|
||||
If no conflicts, or only non-jenkins PEER/DEV conflicts, normal exit(0)
|
||||
|
||||
**********************************************************************************************
|
||||
*********************************************************************************************/
|
||||
|
@ -39,26 +42,30 @@ function initEntry(dependency, version) {
|
|||
|
||||
var packageFiles = [];
|
||||
|
||||
packageFiles.push(require("./blueocean-dashboard/package.json"));
|
||||
packageFiles.push(require("./blueocean-web/package.json"));
|
||||
packageFiles.push(require("../blueocean-dashboard/package.json"));
|
||||
packageFiles.push(require("../blueocean-web/package.json"));
|
||||
packageFiles.push(require("../blueocean-personalization/package.json"));
|
||||
packageFiles.push(require("../blueocean-config/package.json"));
|
||||
packageFiles.push(require("../js-extensions/package.json"));
|
||||
|
||||
// Add some expected dependencies, so we go another level deep just for these
|
||||
packageFiles.push(require("./blueocean-dashboard/node_modules/@jenkins-cd/design-language/package.json"));
|
||||
packageFiles.push(require("./blueocean-dashboard/node_modules/@jenkins-cd/sse-gateway/package.json"));
|
||||
packageFiles.push(require("./blueocean-dashboard/node_modules/@jenkins-cd/js-extensions/package.json"));
|
||||
packageFiles.push(require("../blueocean-dashboard/node_modules/@jenkins-cd/design-language/package.json"));
|
||||
|
||||
packageFiles.forEach(packageFile => {
|
||||
|
||||
addDependencies("prod", packageFile.dependencies);
|
||||
// addDependencies("dev", packageFile.devDependencies);
|
||||
// addDependencies("peer", packageFile.peerDependencies);
|
||||
addDependencies("prod", packageFile.dependencies, true);
|
||||
addDependencies("dev", packageFile.devDependencies, false);
|
||||
addDependencies("peer", packageFile.peerDependencies, false);
|
||||
|
||||
function addDependencies(kind, deps, includeNonJenkins) {
|
||||
|
||||
function addDependencies(kind, deps) {
|
||||
if (deps) {
|
||||
Object.keys(deps).forEach(dependency => {
|
||||
const version = deps[dependency];
|
||||
initEntry(dependency, version);
|
||||
allDependencies[dependency][version].push(packageFile.name + " (" + kind + ")");
|
||||
if (includeNonJenkins || dependency.startsWith("@jenkins-cd")) {
|
||||
const version = deps[dependency];
|
||||
initEntry(dependency, version);
|
||||
allDependencies[dependency][version].push(packageFile.name + " (" + kind + ")");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -80,5 +87,6 @@ Object.keys(allDependencies).forEach(dependency => {
|
|||
|
||||
if (errs.length) {
|
||||
console.error(JSON.stringify(errs, null, 4));
|
||||
console.log('You can use bin/cleanInstall to install the dominant dependency in various places.');
|
||||
process.exitCode = 1;
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/*********************************************************************************************
|
||||
**********************************************************************************************
|
||||
|
||||
Checks for imprecise version numbers in package.json, and compares deps/devDeps between package/shrinkwrap jsons.
|
||||
Usage:
|
||||
|
||||
node checkshrinkwrap.js
|
||||
|
||||
- Any imprecise version number (e.g. ~, ^, >=, etc) in package.json "dependencies" or "devDependencies" will fail
|
||||
- Any dependency in package.json but not in npm-shrinkwrap.json will fail
|
||||
- Any version mismatch in the above will fail
|
||||
|
||||
All failures will exit(1). Otherwise, normal exit(0)
|
||||
|
||||
**********************************************************************************************
|
||||
*********************************************************************************************/
|
||||
|
||||
const fs = require('fs');
|
||||
// match "1.2.3" or "1.2.3-beta5"
|
||||
const PRECISE_VERSION_CHARS_PATTERN = /^\d+\.\d+\.\d+(-[A-Za-z0-9]+)*$/;
|
||||
|
||||
const start = new Date().getTime();
|
||||
|
||||
checkProject('../blueocean-dashboard');
|
||||
checkProject('../blueocean-personalization');
|
||||
checkProject('../blueocean-web');
|
||||
checkProject('../blueocean-config');
|
||||
checkProject('../blueocean-core-js');
|
||||
|
||||
const ellapsed = new Date().getTime() - start;
|
||||
console.log(`all dependencies look good! took ${ellapsed}ms`);
|
||||
// done!
|
||||
|
||||
function checkProject(pathToProject) {
|
||||
const resolvedPath = buildPath(`${__dirname}/${pathToProject}`);
|
||||
console.log(`validating dependencies in ${resolvedPath}`);
|
||||
const packageJsonPath = buildPath(`${resolvedPath}/package.json`);
|
||||
const shrinkwrapJsonPath = buildPath(`${resolvedPath}/npm-shrinkwrap.json`);
|
||||
|
||||
const packages = require(packageJsonPath);
|
||||
const packageDeps = packages.dependencies;
|
||||
const packageDevDeps = packages.devDependencies;
|
||||
|
||||
checkImpreciseDependencies(packageDeps);
|
||||
checkImpreciseDependencies(packageDevDeps);
|
||||
checkDuplicateDependencies(packageDeps, packageDevDeps);
|
||||
|
||||
const allDeps = Object.assign({}, packageDeps, packageDevDeps);
|
||||
const shrinkwrap = require(shrinkwrapJsonPath);
|
||||
validateDepsAgainstShrinkwrap(allDeps, shrinkwrap);
|
||||
console.log('success!');
|
||||
}
|
||||
|
||||
function buildPath(path) {
|
||||
try {
|
||||
return fs.realpathSync(path);
|
||||
} catch (error) {
|
||||
console.error(`ERROR: Could not find ${path}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function checkImpreciseDependencies(dependencies) {
|
||||
const badDeps = [];
|
||||
|
||||
Object.keys(dependencies).forEach(name => {
|
||||
const version = dependencies[name];
|
||||
|
||||
if (!PRECISE_VERSION_CHARS_PATTERN.test(version)) {
|
||||
badDeps.push(`${name}@${version}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (badDeps.length) {
|
||||
badDeps.forEach(dep => console.error(`${dep} must use precise version`));
|
||||
console.error(`did you use 'npm install dep --save/-dev -E' ?`)
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function checkDuplicateDependencies(depList1, depList2) {
|
||||
const keys1 = Object.keys(depList1);
|
||||
const keys2 = Object.keys(depList2);
|
||||
const duplicates = keys1.concat(keys2).filter((name, index, allKeys) => index !== allKeys.indexOf(name));
|
||||
|
||||
if (duplicates.length) {
|
||||
duplicates.forEach(name => console.error(`${name} is already defined in 'dependencies'; remove from 'devDependencies'`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function validateDepsAgainstShrinkwrap(allDeps, shrinkwrap) {
|
||||
const badDeps = [];
|
||||
const shrinkDeps = shrinkwrap.dependencies;
|
||||
|
||||
Object.keys(allDeps).forEach(name => {
|
||||
const version = allDeps[name];
|
||||
|
||||
if (!shrinkDeps[name]) {
|
||||
badDeps.push(`${name}@${version} missing in shrinkwrap`);
|
||||
} else if (shrinkDeps[name].version !== version) {
|
||||
badDeps.push(`${name} should be ${version} but found ${shrinkDeps[name].version}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (badDeps.length) {
|
||||
badDeps.forEach(message => console.error(message));
|
||||
console.log('You can use bin/cleanInstall to install the dominant dependency in various places.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const async = require('async');
|
||||
const exec = require('child_process').exec;
|
||||
const prompt = require('prompt');
|
||||
|
||||
const start = new Date().getTime();
|
||||
const directories = ['../blueocean-dashboard', '../blueocean-personalization', '../blueocean-web'];
|
||||
|
||||
prompt.start();
|
||||
prompt.get({
|
||||
properties: {
|
||||
package: {
|
||||
message: `PACKAGE to install?`,
|
||||
required: true,
|
||||
},
|
||||
version: {
|
||||
message: `VERSION to install?`,
|
||||
required: true,
|
||||
}
|
||||
}
|
||||
}, function (err, result) {
|
||||
// Log the results.
|
||||
console.log('Command-line input received:');
|
||||
console.log('package: ' + result.package);
|
||||
console.log('version: ' + result.version);
|
||||
// const lib = '@jenkins-cd/design-language';
|
||||
// const version = '0.0.79-unpublishedthor1';
|
||||
async.mapSeries(directories, function (elem, callback) {
|
||||
console.log('Current element', elem);
|
||||
removeAndInstall(elem, result.package, result.version, callback);
|
||||
}, function (err, result) {
|
||||
if (err) {
|
||||
console.error('Something went wrong', err);
|
||||
}
|
||||
const ellapsed = new Date().getTime() - start;
|
||||
console.log(`Install look good! took ${ellapsed}ms`);
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
function buildPath(path) {
|
||||
try {
|
||||
return fs.realpathSync(path);
|
||||
} catch (error) {
|
||||
console.error(`ERROR: Could not find ${path}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function removeAndInstall(pathToProject, lib, version, callback) {
|
||||
const resolvedPath = buildPath(`${__dirname}/${pathToProject}`);
|
||||
const removeDir = buildPath(`${resolvedPath}/node_modules/` + lib);
|
||||
if (removeDir !== null) {
|
||||
console.log(`remove dir in ${removeDir}`);
|
||||
deleteFolderRecursive(removeDir);
|
||||
}
|
||||
process.chdir(resolvedPath);
|
||||
console.log('In directory ' + process.cwd());
|
||||
install(lib + '@' + version, callback);
|
||||
}
|
||||
//remove folder Syncronously
|
||||
function deleteFolderRecursive(path) {
|
||||
if (fs.existsSync(path)) {
|
||||
fs.readdirSync(path).forEach(function (file, index) {
|
||||
var curPath = path + "/" + file;
|
||||
if (fs.lstatSync(curPath).isDirectory()) { // recurse
|
||||
deleteFolderRecursive(curPath);
|
||||
} else { // delete file
|
||||
fs.unlinkSync(curPath);
|
||||
}
|
||||
});
|
||||
fs.rmdirSync(path);
|
||||
}
|
||||
}
|
||||
function install(packages, callback) {
|
||||
console.log('installing ', packages);
|
||||
const child = exec('npm install ' + packages + ' --save -E',
|
||||
function (error, stdout, stderr) {
|
||||
if (error !== null) {
|
||||
callback(error);
|
||||
}
|
||||
callback(error, stdout);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env bash
|
||||
set -eu -o pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd -P "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd)"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
usage: $(basename "$0") COMMAND ARGS
|
||||
Helper script to query Git
|
||||
Commands:
|
||||
pr-id Find the Pull Request id
|
||||
Assume that 'git fetch --progress https://github.com/jenkinsci/blueocean-plugin.git +refs/pull/*/head:refs/remotes/origin/pr/*' already ran
|
||||
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
pr-id() {
|
||||
local github_remote=origin
|
||||
local head=$(git rev-parse --verify HEAD)
|
||||
git show-ref | sed -n "s|^$head refs/remotes/${github_remote}/pr/\(.*\)$|\1|p"
|
||||
}
|
||||
|
||||
command_name="$1"; shift; case "$command_name" in
|
||||
pr-id)
|
||||
pr-id "$@"
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
esac
|
|
@ -0,0 +1,83 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
##
|
||||
#
|
||||
# Usage
|
||||
#
|
||||
# jwtcurl [-u username:password] [-b BASE_URL] "[-X GET|POST|PUT|DELETE] BO_API_URL"
|
||||
#
|
||||
# Options:
|
||||
# -v: verbose output
|
||||
# -u: basic auth parameter in username:password format
|
||||
# -b: base url of jenkins without trailing slash. e.g. http://localhost:8080/jenkins or https://blueocean.io
|
||||
#
|
||||
# Note: You need to enclose last argument in double quotes if you are passing arguments to curl.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# Anonymous user:
|
||||
#
|
||||
# jwtcurl http://localhost:8080/jenkins/blue/rest/organizations/jenkins/pipelines/p1/
|
||||
#
|
||||
# User with credentials:
|
||||
#
|
||||
# jwtcurl -u admin:admin http://localhost:8080/jenkins/blue/rest/organizations/jenkins/pipelines/p1/
|
||||
#
|
||||
# Use base url other than http://localhost:8080/jenkins
|
||||
#
|
||||
# jwtcurl -u admin:admin -b https://myjenkinshost http://localhost:8080/jenkins/blue/rest/organizations/jenkins/pipelines/p1/
|
||||
#
|
||||
# Author: Vivek Pandey
|
||||
#
|
||||
##
|
||||
if [ $# -eq 0 ]
|
||||
then
|
||||
echo "Usage: jwtcurl [-v] [-u username:password] [-b BASE_URL] \"-X [GET|POST|PUT|DELETE] BO_API_URL\""
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
while [[ $# -gt 1 ]]
|
||||
do
|
||||
key="$1"
|
||||
|
||||
case $key in
|
||||
-u)
|
||||
CREDENTIAL="-u $2"
|
||||
shift
|
||||
;;
|
||||
-b)
|
||||
BASE_URL="$2"
|
||||
shift
|
||||
;;
|
||||
-v)
|
||||
VERBOSE="$2"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
# unknown option
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [ ! -z "$VERBOSE" ]; then
|
||||
SETX="set -x"
|
||||
CURL_VERBOSE="-v"
|
||||
fi
|
||||
|
||||
if [ -z "${BASE_URL}" ]; then
|
||||
BASE_URL=http://localhost:8080/jenkins
|
||||
fi
|
||||
|
||||
${SETX}
|
||||
|
||||
TOKEN=$(curl ${CURL_VERBOSE} -s -X GET ${CREDENTIAL} -I ${BASE_URL}/jwt-auth/token | awk 'BEGIN {FS=": "}/^X-BLUEOCEAN-JWT/{print $2}'|sed $'s/\r//')
|
||||
|
||||
if [ -z "${TOKEN}" ]; then
|
||||
echo "Failed to get JWT token"
|
||||
echo $?
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl ${CURL_VERBOSE} -H "Authorization: Bearer ${TOKEN}" $@
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
home=`pwd`
|
||||
modules=('blueocean-dashboard' 'blueocean-personalization' 'blueocean-web')
|
||||
echo $home
|
||||
for i in "${modules[@]}"; do
|
||||
cd $home/$i; ncu -a; rm -rf node_modules npm-shrinkwrap.json; npm i; npm shrinkwrap; cd $home
|
||||
done
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "bin",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "cleanInstall.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"async": "^2.0.1",
|
||||
"prompt": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jenkins-cd/design-language": "0.0.79-unpublishedthor1"
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
> Analytics Tools to be injected in to BlueOcean UI
|
||||
|
||||
# RollBar
|
||||
|
||||
* Enable RollBar
|
||||
|
||||
RollBar is disabled by default. Use BLUEOCEAN_ROLLBAR_ENABLED JVM property to enable.
|
||||
|
||||
````
|
||||
mvn hpi:run -DBLUEOCEAN_ROLLBAR_ENABLED=true
|
||||
````
|
||||
|
||||
|
||||
## Usage ...
|
||||
|
||||
try {
|
||||
foo();
|
||||
$blueocean_Rollbar.debug('foo() called');
|
||||
} catch (e) {
|
||||
$blueocean_Rollbar.error('Problem calling foo()', e);
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
var builder = require('@jenkins-cd/js-builder');
|
||||
|
||||
//
|
||||
// Create the rollbar bundle.
|
||||
// See https://github.com/jenkinsci/js-builder
|
||||
//
|
||||
builder.bundle('src/main/js/rollbar.js');
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"name": "blueocean-analytics-tools",
|
||||
"version": "0.0.1",
|
||||
"description": "Analytics tools that gets injected in BlueOcean UI",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Vivek Pandey <vivek.pandey@gmail.com> (https://github.com/vivek)",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jenkins-cd/js-modules": "0.0.5",
|
||||
"rollbar-browser": "1.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jenkins-cd/js-builder": "0.0.35",
|
||||
"babel-eslint": "^6.1.2",
|
||||
"gulp": "3.9.1",
|
||||
"eslint-plugin-react": "^5.0.1"
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package io.jenkins.blueocean.analyticstools;
|
||||
|
||||
import hudson.Extension;
|
||||
import io.jenkins.blueocean.BluePageDecorator;
|
||||
import jenkins.model.Jenkins;
|
||||
|
||||
/**
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
@Extension(ordinal = 10)
|
||||
public class AnalyticsTools extends BluePageDecorator {
|
||||
|
||||
public boolean isRollBarEnabled(){
|
||||
return Boolean.getBoolean("BLUEOCEAN_ROLLBAR_ENABLED");
|
||||
}
|
||||
|
||||
|
||||
/** gives Blueocean plugin version. blueocean-web being core module is looked at to determine the version */
|
||||
public String getBlueOceanPluginVersion(){
|
||||
return Jenkins.getInstance().getPlugin("blueocean-web").getWrapper().getVersion();
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
|
||||
<j:if test="${it.rollBarEnabled}">
|
||||
|
||||
<!--
|
||||
Still use a small bit of JS to inject the Rollbar access token.
|
||||
|
||||
I think this is ok for now because we're setting a global anyway - see
|
||||
comment at the end of src/main/js/rollbar.js
|
||||
-->
|
||||
<script>
|
||||
(function () {
|
||||
window.$$blueocean_pluginVersion = '${it.blueOceanPluginVersion}';
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Running the plugin build (or just "gulp" from the command line) will
|
||||
generate a browser bundle of what's in src/main/js/rollbar.js and we
|
||||
can load that using an adjunct as follows.
|
||||
|
||||
See gulpfile.js and see the output from running the "gulp" command.
|
||||
-->
|
||||
<st:adjunct includes="org.jenkins.ui.jsmodules.blueocean_analytics_tools.rollbar"/>
|
||||
</j:if>
|
||||
</j:jelly>
|
|
@ -0,0 +1,3 @@
|
|||
# Blue Ocean commons
|
||||
|
||||
Common utilities used across modules. Lets try to keep this not a dumping ground ;)
|
|
@ -5,14 +5,14 @@
|
|||
<parent>
|
||||
<groupId>io.jenkins.blueocean</groupId>
|
||||
<artifactId>blueocean-parent</artifactId>
|
||||
<version>1.0-alpha-7-SNAPSHOT</version>
|
||||
<version>1.0.0-b16-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>blueocean-commons</artifactId>
|
||||
<packaging>hpi</packaging>
|
||||
|
||||
<name>BlueOcean :: Commons API</name>
|
||||
|
||||
<name>Common API for Blue Ocean</name>
|
||||
|
||||
<url>https://wiki.jenkins-ci.org/display/JENKINS/Blue+Ocean+Plugin</url>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package io.jenkins.blueocean.commons;
|
||||
|
||||
/**
|
||||
* Common place put system properties that are used by blueocean modules.
|
||||
*
|
||||
* @author Ivan Meredith
|
||||
*/
|
||||
public class BlueOceanConfigProperties {
|
||||
public static final boolean ROLLBAR_ENABLED = Boolean.getBoolean("BLUEOCEAN_ROLLBAR_ENABLED");
|
||||
|
||||
public static final boolean BLUEOCEAN_FEATURE_JWT_AUTHENTICATION = Boolean.getBoolean("BLUEOCEAN_FEATURE_JWT_AUTHENTICATION");
|
||||
|
||||
}
|
|
@ -0,0 +1,294 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2016, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package io.jenkins.blueocean.commons;
|
||||
|
||||
import hudson.model.Run;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.NoExternalUse;
|
||||
import org.kohsuke.stapler.Stapler;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
|
||||
import javax.annotation.CheckForNull;
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* General purpose Blue Ocean UI URL parser.
|
||||
* <p>
|
||||
* This class performs a "best effort" attempt to parse a URL as a Blue Ocean
|
||||
* URL, extracting what it thinks are the relevant "parts" and making available via the
|
||||
* {@link #getPart(UrlPart)} and {@link #hasPart(UrlPart)} functions.
|
||||
* <p>
|
||||
* See TBD comment on {@link UrlPart}.
|
||||
*
|
||||
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
|
||||
*/
|
||||
@Restricted(NoExternalUse.class) // Internal use only for now because there's a fair chance we'll change how it works. See TBD comment on UrlPart.
|
||||
public class BlueUrlTokenizer {
|
||||
|
||||
private static final Set<String> PIPELINE_TABS =
|
||||
new LinkedHashSet<>(Arrays.asList("activity", "branches", "pr"));
|
||||
|
||||
private static final Set<String> PIPELINE_RUN_DETAIL_TABS =
|
||||
new LinkedHashSet<>(Arrays.asList("pipeline", "changes", "tests", "artifacts"));
|
||||
|
||||
private Map<UrlPart, String> urlParts = new LinkedHashMap<>();
|
||||
private UrlPart lastPart;
|
||||
|
||||
/**
|
||||
* Enum of URL "parts".
|
||||
* <p>
|
||||
* Use {@link #getPart(UrlPart)} to get a specific URL "part",
|
||||
* or call {@link #hasPart(UrlPart)} to check for it's existence.
|
||||
* <p>
|
||||
* *** TBD: decide whether to stick with this model, or to switch to more of a straight getters/setters style on the {@link BlueUrlTokenizer} instance.
|
||||
* Reason for trying this approach ("parts" enum) is that I (TF) think the straight properties style with getters/setters
|
||||
* would get messy as we add support for parsing more URL paths/parts i.e. a getters/setters API explosion.
|
||||
* That said ... not sure I love this approach either, hence marked BlueoceanUrl as @Restricted(NoExternalUse.class). Let's suck it
|
||||
* and see for a bit and change if it sucks :)
|
||||
*/
|
||||
public enum UrlPart {
|
||||
/**
|
||||
* Main blue ocean pipelines dashboard.
|
||||
* i.e. /blue/pipelines/
|
||||
*/
|
||||
DASHBOARD_PIPELINES,
|
||||
/**
|
||||
* A URL pointing at a page associated with an "organization" resource.
|
||||
* e.g. /blue/organizations/jenkins/...
|
||||
* <p>
|
||||
* Call Use {@link #getPart(UrlPart)} to get the organization name.
|
||||
*/
|
||||
ORGANIZATION,
|
||||
/**
|
||||
* A URL pointing at a pipeline.
|
||||
* e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/...
|
||||
* <p>
|
||||
* Call Use {@link #getPart(UrlPart)} to get the pipeline name. Note that the URL
|
||||
* may have additional parts (e.g. {@link UrlPart#PIPELINE_TAB} or {@link UrlPart#PIPELINE_RUN_DETAIL}).
|
||||
*/
|
||||
PIPELINE,
|
||||
/**
|
||||
* A URL pointing at a pipeline tab.
|
||||
* e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/activity
|
||||
* <p>
|
||||
* Call Use {@link #getPart(UrlPart)} to get the tab name.
|
||||
*/
|
||||
PIPELINE_TAB,
|
||||
/**
|
||||
* A URL pointing at a pipeline Run Details.
|
||||
* e.g. // e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/detail/...
|
||||
* <p>
|
||||
* See {@link #BRANCH} for sub-component of this URL.
|
||||
*/
|
||||
PIPELINE_RUN_DETAIL,
|
||||
/**
|
||||
* A URL pointing at a pipeline Run Details for a specific branch.
|
||||
* e.g. // e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/detail/magic-branch-X/...
|
||||
* <p>
|
||||
* See {@link #PIPELINE_RUN_DETAIL_ID} for sub-component of this URL.
|
||||
* <p>
|
||||
* Call Use {@link #getPart(UrlPart)} to get the branch name.
|
||||
*/
|
||||
BRANCH,
|
||||
/**
|
||||
* A URL pointing at a pipeline Run Details for a specific run of a specific branch.
|
||||
* e.g. // e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/detail/magic-branch-X/55/...
|
||||
* <p>
|
||||
* See {@link #PIPELINE_RUN_DETAIL_ID} for sub-component of this URL.
|
||||
* <p>
|
||||
* Call Use {@link #getPart(UrlPart)} to get the {@link Run} ID.
|
||||
*/
|
||||
PIPELINE_RUN_DETAIL_ID,
|
||||
/**
|
||||
* A URL pointing at one of the tabs on a pipeline Run Details for a specific run of a specific branch.
|
||||
* e.g. // e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/detail/magic-branch-X/55/artifacts
|
||||
* <p>
|
||||
* Call Use {@link #getPart(UrlPart)} to get the tab name.
|
||||
*/
|
||||
PIPELINE_RUN_DETAIL_TAB,
|
||||
}
|
||||
|
||||
private BlueUrlTokenizer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the {@link Stapler#getCurrentRequest() current Stapler request} and return a {@link BlueUrlTokenizer} instance
|
||||
* iff the URL is a Blue Ocean UI URL.
|
||||
*
|
||||
* @return A {@link BlueUrlTokenizer} instance iff the URL is a Blue Ocean UI URL, otherwise {@code null}.
|
||||
* @throws IllegalStateException Called outside the scope of an active {@link StaplerRequest}.
|
||||
*/
|
||||
public static @CheckForNull
|
||||
BlueUrlTokenizer parseCurrentRequest() throws IllegalStateException {
|
||||
StaplerRequest currentRequest = Stapler.getCurrentRequest();
|
||||
|
||||
if (currentRequest == null) {
|
||||
throw new IllegalStateException("Illegal call to BlueoceanUrl.parseCurrentRequest outside the scope of an active StaplerRequest.");
|
||||
}
|
||||
|
||||
String path = currentRequest.getOriginalRequestURI();
|
||||
String contextPath = currentRequest.getContextPath();
|
||||
|
||||
path = path.substring(contextPath.length());
|
||||
|
||||
return parse(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the supplied URL string and return a {@link BlueUrlTokenizer} instance
|
||||
* iff the URL is a Blue Ocean UI URL.
|
||||
*
|
||||
* @param url The URL to be parsed. The URL must not be decoded in any way, so as to ensure
|
||||
* that no URL component data is lost.
|
||||
* @return A {@link BlueUrlTokenizer} instance iff the URL is a Blue Ocean UI URL, otherwise {@code null}.
|
||||
*/
|
||||
public static @CheckForNull
|
||||
BlueUrlTokenizer parse(@Nonnull String url) {
|
||||
Iterator<String> urlTokens = extractTokens(url);
|
||||
|
||||
//
|
||||
// Yes, the following code is quite ugly, but it's easy enough to understand atm.
|
||||
// Unless this gets a lot more detailed, please don't get super clever ideas about using
|
||||
// some fancy-pants abstractions/patterns/3rd-party-libs for parsing the URL that, while
|
||||
// might make the code look neater structurally, also makes the code logic a lot harder
|
||||
// to follow (without using a debugger).
|
||||
//
|
||||
if (urlTokens.hasNext()) {
|
||||
if (urlTokens.next().equalsIgnoreCase("blue")) {
|
||||
BlueUrlTokenizer blueUrlTokenizer = new BlueUrlTokenizer();
|
||||
|
||||
if (urlTokens.hasNext()) {
|
||||
String next = urlTokens.next();
|
||||
|
||||
if (next.equalsIgnoreCase("pipelines")) {
|
||||
// i.e. /blue/pipelines/
|
||||
blueUrlTokenizer.addPart(UrlPart.DASHBOARD_PIPELINES, next);
|
||||
} else if (next.equalsIgnoreCase("organizations")) {
|
||||
// i.e. /blue/organizations/...
|
||||
if (urlTokens.hasNext()) {
|
||||
// e.g. /blue/organizations/jenkins/...
|
||||
blueUrlTokenizer.addPart(UrlPart.ORGANIZATION, urlTokens.next());
|
||||
if (urlTokens.hasNext()) {
|
||||
// e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/...
|
||||
blueUrlTokenizer.addPart(UrlPart.PIPELINE, urlDecode(urlTokens.next()));
|
||||
if (urlTokens.hasNext()) {
|
||||
next = urlTokens.next();
|
||||
if (next.equalsIgnoreCase("detail")) {
|
||||
// e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/detail/...
|
||||
blueUrlTokenizer.addPart(UrlPart.PIPELINE_RUN_DETAIL, next);
|
||||
if (urlTokens.hasNext()) {
|
||||
// e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/detail/magic-branch-X/...
|
||||
blueUrlTokenizer.addPart(UrlPart.BRANCH, urlDecode(urlTokens.next()));
|
||||
if (urlTokens.hasNext()) {
|
||||
// e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/detail/magic-branch-X/55/...
|
||||
blueUrlTokenizer.addPart(UrlPart.PIPELINE_RUN_DETAIL_ID, urlDecode(urlTokens.next()));
|
||||
if (urlTokens.hasNext()) {
|
||||
next = urlTokens.next();
|
||||
if (PIPELINE_RUN_DETAIL_TABS.contains(next.toLowerCase())) {
|
||||
// e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/detail/magic-branch-X/55/pipeline
|
||||
blueUrlTokenizer.addPart(UrlPart.PIPELINE_RUN_DETAIL_TAB, next.toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (PIPELINE_TABS.contains(next.toLowerCase())) {
|
||||
// e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/activity/
|
||||
blueUrlTokenizer.addPart(UrlPart.PIPELINE_TAB, next.toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return blueUrlTokenizer;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void addPart(@Nonnull UrlPart urlPart, @Nonnull String value) {
|
||||
urlParts.put(urlPart, value);
|
||||
this.lastPart = urlPart;
|
||||
}
|
||||
|
||||
public boolean hasPart(@Nonnull UrlPart urlPart) {
|
||||
return urlParts.containsKey(urlPart);
|
||||
}
|
||||
|
||||
public @CheckForNull String getPart(@Nonnull UrlPart urlPart) {
|
||||
return urlParts.get(urlPart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last {@link UrlPart} for the URL.
|
||||
* @return The last {@link UrlPart} for the URL.
|
||||
*/
|
||||
public @CheckForNull UrlPart getLastPart() {
|
||||
return this.lastPart;
|
||||
}
|
||||
|
||||
public boolean lastPartIs(@Nonnull UrlPart urlPart) {
|
||||
return this.lastPart == urlPart;
|
||||
}
|
||||
|
||||
public boolean lastPartIs(@Nonnull UrlPart urlPart, @Nonnull String value) {
|
||||
if (this.lastPart == urlPart) {
|
||||
return getPart(this.lastPart).equals(value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String urlDecode(String string) {
|
||||
try {
|
||||
return URLDecoder.decode(string, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new IllegalStateException("Unexpected UnsupportedEncodingException for UTF-8.");
|
||||
}
|
||||
}
|
||||
|
||||
private static Iterator<String> extractTokens(String url) {
|
||||
String[] uncleanedTokens = url.split("/");
|
||||
List<String> cleanedTokens = new ArrayList<>();
|
||||
|
||||
for (String uncleanedToken : uncleanedTokens) {
|
||||
if (uncleanedToken.length() != 0) {
|
||||
cleanedTokens.add(uncleanedToken);
|
||||
}
|
||||
}
|
||||
|
||||
return cleanedTokens.iterator();
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package io.jenkins.blueocean.commons;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.core.JsonParseException;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
|
@ -7,6 +8,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
|
|||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.databind.introspect.VisibilityChecker;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -60,7 +62,12 @@ public class JsonConverter{
|
|||
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
|
||||
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
mapper.setVisibilityChecker(
|
||||
new VisibilityChecker.Std(JsonAutoDetect.Visibility.NONE,
|
||||
JsonAutoDetect.Visibility.NONE,
|
||||
JsonAutoDetect.Visibility.NONE,
|
||||
JsonAutoDetect.Visibility.NONE,
|
||||
JsonAutoDetect.Visibility.ANY));
|
||||
return mapper;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2016, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package io.jenkins.blueocean.commons;
|
||||
|
||||
import hudson.ExtensionList;
|
||||
import org.apache.tools.ant.ExtensionPoint;
|
||||
|
||||
import javax.annotation.CheckForNull;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* Page state "preloader" extension point.
|
||||
* <p>
|
||||
* Allows the loading page's JavaScript blueocean global scope to
|
||||
* be pre-populated with data that we know the page is going to need, thereby
|
||||
* providing a mechanism for eliminating the request overhead for that data.
|
||||
*
|
||||
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
|
||||
*/
|
||||
public abstract class PageStatePreloader extends ExtensionPoint {
|
||||
|
||||
/**
|
||||
* Get the JavaScript object graph path at shiwh the state is to be stored.
|
||||
* @return The JavaScript object graph path at shiwh the state is to be stored.
|
||||
*/
|
||||
@Nonnull
|
||||
public abstract String getStatePropertyPath();
|
||||
|
||||
/**
|
||||
* Get the state JSON to be set in the page's JavaScript blueocean global scope.
|
||||
* @return The state JSON to be set in the page's JavaScript blueocean global
|
||||
* scope, or {@code null} if no data is to be set of this page.
|
||||
*/
|
||||
@CheckForNull
|
||||
public abstract String getStateJson();
|
||||
|
||||
public static ExtensionList<PageStatePreloader> all() {
|
||||
return ExtensionList.lookup(PageStatePreloader.class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2016, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package io.jenkins.blueocean.commons;
|
||||
|
||||
import net.sf.json.JSONObject;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* REST prefetch data preloader.
|
||||
* <p>
|
||||
* Pre-populates the page with REST data, allowing the client side {@code Fetch}
|
||||
* module (see {@code Fetch} module in the {@code @jenkins-cd/blueocean-core-js NPM packages})
|
||||
* to avoid the REST API call overhead.
|
||||
* <p>
|
||||
* Create implementations of this class (and annotate with {@code @Extension}) for data that
|
||||
* we know is going to be needed by the page.
|
||||
*
|
||||
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
|
||||
*/
|
||||
public abstract class RESTFetchPreloader extends PageStatePreloader {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public final String getStatePropertyPath() {
|
||||
return "prefetchdata." + getClass().getSimpleName();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public final String getStateJson() {
|
||||
BlueUrlTokenizer blueUrl = BlueUrlTokenizer.parseCurrentRequest();
|
||||
|
||||
if (blueUrl == null) {
|
||||
// Not a Blue Ocean page, so nothing to be added.
|
||||
return null;
|
||||
}
|
||||
|
||||
FetchData fetchData = getFetchData(blueUrl);
|
||||
if (fetchData != null) {
|
||||
return fetchData.toJSON();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract FetchData getFetchData(@Nonnull BlueUrlTokenizer blueUrl);
|
||||
|
||||
public static final class FetchData {
|
||||
|
||||
private String restUrl;
|
||||
private String data;
|
||||
|
||||
public FetchData(@Nonnull String restUrl, @Nonnull String data) {
|
||||
this.restUrl = restUrl;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public String getRestUrl() {
|
||||
return restUrl;
|
||||
}
|
||||
|
||||
public String getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public String toJSON() {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("restUrl", restUrl);
|
||||
json.put("data", data);
|
||||
return json.toString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2016, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package io.jenkins.blueocean.commons.stapler;
|
||||
|
||||
import org.kohsuke.stapler.export.ExportConfig;
|
||||
import org.kohsuke.stapler.export.Flavor;
|
||||
import org.kohsuke.stapler.export.Model;
|
||||
import org.kohsuke.stapler.export.ModelBuilder;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
|
||||
/**
|
||||
* Simple Jenkins Model Object serializer.
|
||||
*
|
||||
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
|
||||
*/
|
||||
public class ModelObjectSerializer {
|
||||
|
||||
private static ExportConfig config = new ExportConfig();
|
||||
|
||||
private ModelObjectSerializer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the supplied object to JSON and return as a {@link String}.
|
||||
* @param object The object to serialize.
|
||||
* @return The JSON as a {@link String}.
|
||||
* @throws IOException Error serializing model object.
|
||||
*/
|
||||
@Nonnull
|
||||
public static String toJson(@Nonnull Object object) throws IOException {
|
||||
try (StringWriter writer = new StringWriter()) {
|
||||
toJson(object, writer);
|
||||
return writer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the supplied object to JSON and write to the supplied {@link Writer}.
|
||||
* @param object The object to serialize.
|
||||
* @param writer The writer to output to.
|
||||
* @throws IOException Error serializing model object.
|
||||
*/
|
||||
public static void toJson(@Nonnull Object object, @Nonnull Writer writer) throws IOException {
|
||||
Model model = new ModelBuilder().get(object.getClass());
|
||||
model.writeTo(object, Flavor.JSON.createDataWriter(object, writer, config));
|
||||
writer.flush();
|
||||
}
|
||||
}
|
|
@ -1,14 +1,17 @@
|
|||
package io.jenkins.blueocean.commons.stapler;
|
||||
|
||||
import hudson.model.Api;
|
||||
import org.kohsuke.stapler.CancelRequestHandlingException;
|
||||
import org.kohsuke.stapler.HttpResponse;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
import org.kohsuke.stapler.StaplerResponse;
|
||||
import org.kohsuke.stapler.interceptor.Interceptor;
|
||||
import org.kohsuke.stapler.interceptor.InterceptorAnnotation;
|
||||
import org.kohsuke.stapler.verb.HttpVerbInterceptor;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
@ -31,14 +34,41 @@ public @interface TreeResponse {
|
|||
public Object invoke(StaplerRequest request, StaplerResponse response, Object instance, Object[] arguments)
|
||||
throws IllegalAccessException, InvocationTargetException, ServletException {
|
||||
|
||||
final Object resp = target.invoke(request, response, instance, arguments);
|
||||
/**
|
||||
* If request.method and HTTP verb annotations {@link GET}, {@link POST}, {@link PUT} and {@link DELETE}
|
||||
* do not match it skips invoking this target. If there no such annotations present then GET as default is
|
||||
* assumed and request is dispatched to target.
|
||||
*/
|
||||
if (matches(request)) {
|
||||
final Object resp = target.invoke(request, response, instance, arguments);
|
||||
|
||||
return new HttpResponse() {
|
||||
@Override
|
||||
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {
|
||||
new Api(resp).doJson(req, rsp);
|
||||
return new HttpResponse() {
|
||||
@Override
|
||||
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {
|
||||
new Api(resp).doJson(req, rsp);
|
||||
}
|
||||
};
|
||||
}else{
|
||||
throw new CancelRequestHandlingException();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean matches(StaplerRequest request) {
|
||||
String method = request.getMethod();
|
||||
|
||||
for (Annotation a : target.getAnnotations()) {
|
||||
Class<? extends Annotation> t = a.annotationType();
|
||||
InterceptorAnnotation ia = t.getAnnotation(InterceptorAnnotation.class);
|
||||
if (ia !=null && ia.value()==HttpVerbInterceptor.class) {
|
||||
return t.getName().endsWith(method);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
//by default we treat it as GET
|
||||
if(method.equals("GET")){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2016, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package io.jenkins.blueocean.commons;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
|
||||
*/
|
||||
public class BlueUrlTokenizerTest {
|
||||
|
||||
@Test
|
||||
public void test_MalformedURLException() {
|
||||
Assert.assertNull(BlueUrlTokenizer.parse("/a"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
BlueUrlTokenizer blueUrl;
|
||||
|
||||
blueUrl = BlueUrlTokenizer.parse("/blue/pipelines/");
|
||||
Assert.assertEquals("pipelines", blueUrl.getPart(BlueUrlTokenizer.UrlPart.DASHBOARD_PIPELINES));
|
||||
Assert.assertEquals(BlueUrlTokenizer.UrlPart.DASHBOARD_PIPELINES, blueUrl.getLastPart());
|
||||
Assert.assertTrue(blueUrl.lastPartIs(BlueUrlTokenizer.UrlPart.DASHBOARD_PIPELINES));
|
||||
|
||||
blueUrl = BlueUrlTokenizer.parse("/blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/activity/");
|
||||
Assert.assertFalse(blueUrl.hasPart(BlueUrlTokenizer.UrlPart.DASHBOARD_PIPELINES));
|
||||
Assert.assertTrue(blueUrl.hasPart(BlueUrlTokenizer.UrlPart.ORGANIZATION));
|
||||
Assert.assertEquals("jenkins", blueUrl.getPart(BlueUrlTokenizer.UrlPart.ORGANIZATION));
|
||||
Assert.assertEquals("f1/f3 with spaces/f3 pipeline", blueUrl.getPart(BlueUrlTokenizer.UrlPart.PIPELINE));
|
||||
Assert.assertEquals("activity", blueUrl.getPart(BlueUrlTokenizer.UrlPart.PIPELINE_TAB));
|
||||
Assert.assertEquals(BlueUrlTokenizer.UrlPart.PIPELINE_TAB, blueUrl.getLastPart());
|
||||
Assert.assertTrue(blueUrl.lastPartIs(BlueUrlTokenizer.UrlPart.PIPELINE_TAB, "activity"));
|
||||
|
||||
blueUrl = BlueUrlTokenizer.parse("/blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/detail/magic-branch-X/55/pipeline");
|
||||
Assert.assertFalse(blueUrl.hasPart(BlueUrlTokenizer.UrlPart.DASHBOARD_PIPELINES));
|
||||
Assert.assertTrue(blueUrl.hasPart(BlueUrlTokenizer.UrlPart.ORGANIZATION));
|
||||
Assert.assertEquals("jenkins", blueUrl.getPart(BlueUrlTokenizer.UrlPart.ORGANIZATION));
|
||||
Assert.assertEquals("f1/f3 with spaces/f3 pipeline", blueUrl.getPart(BlueUrlTokenizer.UrlPart.PIPELINE));
|
||||
Assert.assertFalse(blueUrl.hasPart(BlueUrlTokenizer.UrlPart.PIPELINE_TAB));
|
||||
Assert.assertTrue(blueUrl.hasPart(BlueUrlTokenizer.UrlPart.PIPELINE_RUN_DETAIL));
|
||||
Assert.assertEquals("magic-branch-X", blueUrl.getPart(BlueUrlTokenizer.UrlPart.BRANCH));
|
||||
Assert.assertEquals("55", blueUrl.getPart(BlueUrlTokenizer.UrlPart.PIPELINE_RUN_DETAIL_ID));
|
||||
Assert.assertEquals("pipeline", blueUrl.getPart(BlueUrlTokenizer.UrlPart.PIPELINE_RUN_DETAIL_TAB));
|
||||
Assert.assertEquals(BlueUrlTokenizer.UrlPart.PIPELINE_RUN_DETAIL_TAB, blueUrl.getLastPart());
|
||||
Assert.assertTrue(blueUrl.lastPartIs(BlueUrlTokenizer.UrlPart.PIPELINE_RUN_DETAIL_TAB, "pipeline"));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2016, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package io.jenkins.blueocean.commons.stapler;
|
||||
|
||||
import net.sf.json.JSONObject;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.kohsuke.stapler.export.Exported;
|
||||
import org.kohsuke.stapler.export.ExportedBean;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
|
||||
*/
|
||||
public class ModelObjectSerializerTest {
|
||||
|
||||
@Test
|
||||
public void test_json() throws IOException {
|
||||
String xJson = ModelObjectSerializer.toJson(new X());
|
||||
JSONObject jsonObj = JSONObject.fromObject(xJson);
|
||||
|
||||
Assert.assertEquals(X.class.getName(), jsonObj.getString("_class"));
|
||||
Assert.assertEquals("xVal", jsonObj.getString("val"));
|
||||
}
|
||||
|
||||
@ExportedBean
|
||||
public static class X {
|
||||
@Exported
|
||||
public String val() {
|
||||
return "xVal";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
max_line_length = 160
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
|
@ -0,0 +1 @@
|
|||
Simple marker file to tell maven to execute node build profiles.
|
|
@ -0,0 +1,39 @@
|
|||
> BlueOcean Config plugin
|
||||
|
||||
# BlueOcean configuration
|
||||
|
||||
BlueOcean configuration is injected as $blueOceanConfig JS object. It's in the following JSON format:
|
||||
|
||||
{
|
||||
"version" : "1.0-alpha-7-SNAPSHOT (private-33ee8e40-vivek)",
|
||||
"jenkinsConfig" : {
|
||||
"version" : "2.2",
|
||||
"security" : {
|
||||
"authorizationStrategy" : {
|
||||
"allowAnonymousRead" : true
|
||||
},
|
||||
"enabled" : true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# RollBar
|
||||
|
||||
* Enable RollBar
|
||||
|
||||
RollBar is disabled by default. Use BLUEOCEAN_ROLLBAR_ENABLED JVM property to enable.
|
||||
|
||||
````
|
||||
mvn hpi:run -DBLUEOCEAN_ROLLBAR_ENABLED=true
|
||||
````
|
||||
|
||||
|
||||
## Usage ...
|
||||
|
||||
try {
|
||||
foo();
|
||||
$blueocean_Rollbar.debug('foo() called');
|
||||
} catch (e) {
|
||||
$blueocean_Rollbar.error('Problem calling foo()', e);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "blueocean-config",
|
||||
"version": "0.0.1",
|
||||
"description": "Configuration injected in BlueOcean UI",
|
||||
"scripts": {
|
||||
"build": "jjsbuilder",
|
||||
"mvnbuild": "jjsbuilder --tasks bundle",
|
||||
"mvntest": "jjsbuilder --tasks test,lint"
|
||||
},
|
||||
"author": "Vivek Pandey <vivek.pandey@gmail.com> (https://github.com/vivek)",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jenkins-cd/js-modules": "0.0.8",
|
||||
"rollbar-browser": "1.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jenkins-cd/js-builder": "0.0.50",
|
||||
"babel-eslint": "6.1.2",
|
||||
"eslint-plugin-react": "4.3.0",
|
||||
"gulp": "3.9.1"
|
||||
},
|
||||
"jenkinscd": {
|
||||
"bundle": [
|
||||
"src/main/js/rollbar.js"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -5,13 +5,13 @@
|
|||
<parent>
|
||||
<groupId>io.jenkins.blueocean</groupId>
|
||||
<artifactId>blueocean-parent</artifactId>
|
||||
<version>1.0-alpha-7-SNAPSHOT</version>
|
||||
<version>1.0.0-b16-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>blueocean-analytics-tools</artifactId>
|
||||
<artifactId>blueocean-config</artifactId>
|
||||
<packaging>hpi</packaging>
|
||||
|
||||
<name>BlueOcean :: Analytics Tools</name>
|
||||
<name>Config API for Blue Ocean</name>
|
||||
<url>https://wiki.jenkins-ci.org/display/JENKINS/Blue+Ocean+Plugin</url>
|
||||
|
||||
<dependencies>
|
||||
|
@ -19,5 +19,13 @@
|
|||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>blueocean-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>blueocean-rest-impl</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>blueocean-commons</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -0,0 +1,71 @@
|
|||
package io.jenkins.blueocean.config;
|
||||
|
||||
import hudson.Extension;
|
||||
import hudson.security.AuthorizationStrategy;
|
||||
import hudson.security.FullControlOnceLoggedInAuthorizationStrategy;
|
||||
import hudson.security.SecurityRealm;
|
||||
import io.jenkins.blueocean.commons.BlueOceanConfigProperties;
|
||||
import io.jenkins.blueocean.commons.PageStatePreloader;
|
||||
import jenkins.model.Jenkins;
|
||||
import net.sf.json.util.JSONBuilder;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
@Extension
|
||||
public class BlueOceanConfigStatePreloader extends PageStatePreloader {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(BlueOceanConfigStatePreloader.class.getName());
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getStatePropertyPath() {
|
||||
return "config";
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getStateJson() {
|
||||
StringWriter writer = new StringWriter();
|
||||
Jenkins jenkins = Jenkins.getInstance();
|
||||
String version = Jenkins.getVersion() != null ? Jenkins.getVersion().toString() : Jenkins.VERSION;
|
||||
|
||||
AuthorizationStrategy authorizationStrategy = jenkins.getAuthorizationStrategy();
|
||||
boolean allowAnonymousRead = true;
|
||||
if(authorizationStrategy instanceof FullControlOnceLoggedInAuthorizationStrategy){
|
||||
allowAnonymousRead = ((FullControlOnceLoggedInAuthorizationStrategy) authorizationStrategy).isAllowAnonymousRead();
|
||||
}
|
||||
|
||||
new JSONBuilder(writer)
|
||||
.object()
|
||||
.key("version").value(getBlueOceanPluginVersion())
|
||||
.key("jenkinsConfig")
|
||||
.object()
|
||||
.key("version").value(version)
|
||||
.key("security")
|
||||
.object()
|
||||
.key("enabled").value(jenkins.isUseSecurity())
|
||||
.key("loginUrl").value(jenkins.getSecurityRealm() == SecurityRealm.NO_AUTHENTICATION ? null : jenkins.getSecurityRealm().getLoginUrl())
|
||||
.key("authorizationStrategy").object()
|
||||
.key("allowAnonymousRead").value(allowAnonymousRead)
|
||||
.endObject()
|
||||
.key("enableJWT").value(BlueOceanConfigProperties.BLUEOCEAN_FEATURE_JWT_AUTHENTICATION)
|
||||
.endObject()
|
||||
.endObject()
|
||||
.endObject();
|
||||
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
/** gives Blueocean plugin version. blueocean-web being core module is looked at to determine the version */
|
||||
private String getBlueOceanPluginVersion(){
|
||||
return Jenkins.getInstance().getPlugin("blueocean-web").getWrapper().getVersion();
|
||||
}
|
||||
}
|
|
@ -21,27 +21,24 @@
|
|||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package io.jenkins.blueocean.jsextensions;
|
||||
package io.jenkins.blueocean.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.NoExternalUse;
|
||||
import org.kohsuke.stapler.HttpResponse;
|
||||
import org.kohsuke.stapler.WebMethod;
|
||||
import org.kohsuke.stapler.verb.GET;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -49,8 +46,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||
|
||||
import hudson.Extension;
|
||||
import hudson.PluginWrapper;
|
||||
import hudson.util.HttpResponses;
|
||||
import io.jenkins.blueocean.RootRoutable;
|
||||
import io.jenkins.blueocean.rest.model.BlueExtensionClass;
|
||||
import io.jenkins.blueocean.rest.model.BlueExtensionClassContainer;
|
||||
import jenkins.model.Jenkins;
|
||||
|
@ -58,14 +53,18 @@ import net.sf.json.JSONArray;
|
|||
|
||||
/**
|
||||
* Utility class for gathering {@code jenkins-js-extension} data.
|
||||
*
|
||||
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
|
||||
*/
|
||||
@Extension
|
||||
@Restricted(NoExternalUse.class)
|
||||
@SuppressWarnings({"rawtypes","unchecked"})
|
||||
public class JenkinsJSExtensions implements RootRoutable {
|
||||
public class JenkinsJSExtensions {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(JenkinsJSExtensions.class);
|
||||
|
||||
private static final String PLUGIN_ID = "hpiPluginId";
|
||||
private static final String PLUGIN_VER = "hpiPluginVer";
|
||||
private static final String PLUGIN_EXT = "extensions";
|
||||
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
|
@ -79,25 +78,13 @@ public class JenkinsJSExtensions implements RootRoutable {
|
|||
|
||||
private static final Map<String, Object> jsExtensionCache = new ConcurrentHashMap<>();
|
||||
|
||||
public JenkinsJSExtensions() {
|
||||
}
|
||||
|
||||
/**
|
||||
* For the location in the API: /blue/js-extensions
|
||||
*/
|
||||
@Override
|
||||
public String getUrlName() {
|
||||
return "js-extensions";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the actual data, from /js-extensions
|
||||
*/
|
||||
@WebMethod(name="") @GET
|
||||
public HttpResponse doData() {
|
||||
public static JSONArray getExtensionsData() {
|
||||
Object jsExtensionData = getJenkinsJSExtensionData();
|
||||
JSONArray jsExtensionDataJson = JSONArray.fromObject(jsExtensionData);
|
||||
return HttpResponses.okJSON(jsExtensionDataJson);
|
||||
return jsExtensionDataJson;
|
||||
}
|
||||
|
||||
/*protected*/ static Collection<Object> getJenkinsJSExtensionData() {
|
||||
|
@ -106,7 +93,7 @@ public class JenkinsJSExtensions implements RootRoutable {
|
|||
}
|
||||
|
||||
private static String getGav(Map ext){
|
||||
return ext.get("hpiPluginId") != null ? (String)ext.get("hpiPluginId") : null;
|
||||
return (String) ext.get(PLUGIN_ID);
|
||||
}
|
||||
|
||||
private static void refreshCacheIfNeeded(){
|
||||
|
@ -122,54 +109,86 @@ public class JenkinsJSExtensions implements RootRoutable {
|
|||
refreshCache(pluginCache);
|
||||
}
|
||||
for (PluginWrapper pluginWrapper : pluginCache) {
|
||||
//skip if not active
|
||||
if (!pluginWrapper.isActive()) {
|
||||
continue;
|
||||
}
|
||||
//skip probing plugin if already read
|
||||
if (jsExtensionCache.get(pluginWrapper.getLongName()) != null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
Enumeration<URL> dataResources = pluginWrapper.classLoader.getResources("jenkins-js-extension.json");
|
||||
boolean hasDefinedExtensions = false;
|
||||
|
||||
while (dataResources.hasMoreElements()) {
|
||||
URL dataRes = dataResources.nextElement();
|
||||
StringWriter fileContentBuffer = new StringWriter();
|
||||
|
||||
LOGGER.debug("Reading 'jenkins-js-extension.json' from '{}'.", dataRes);
|
||||
|
||||
try {
|
||||
IOUtils.copy(dataRes.openStream(), fileContentBuffer, Charset.forName("UTF-8"));
|
||||
Map<?,List<Map>> extensionData = mapper.readValue(dataRes.openStream(), Map.class);
|
||||
List<Map> extensions = (List<Map>)extensionData.get("extensions");
|
||||
try (InputStream dataResStream = dataRes.openStream()) {
|
||||
Map<String, Object> extensionData = mapper.readValue(dataResStream, Map.class);
|
||||
|
||||
String pluginId = getGav(extensionData);
|
||||
if (pluginId != null) {
|
||||
// Skip if the plugin name specified on the extension data does not match the name
|
||||
// on the PluginWrapper for this iteration. This can happen for e.g. aggregator
|
||||
// plugins, in which case you'll be seeing extension resources on it's dependencies.
|
||||
// We can skip these here because we will process those plugins themselves in a
|
||||
// future/past iteration of this loop.
|
||||
if (!pluginId.equals(pluginWrapper.getShortName())) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
LOGGER.error(String.format("Plugin %s JS extension has missing hpiPluginId", pluginWrapper.getLongName()));
|
||||
continue;
|
||||
}
|
||||
|
||||
List<Map> extensions = (List<Map>) extensionData.get(PLUGIN_EXT);
|
||||
for (Map extension : extensions) {
|
||||
try {
|
||||
String type = (String)extension.get("type");
|
||||
String type = (String) extension.get("type");
|
||||
if (type != null) {
|
||||
BlueExtensionClassContainer extensionClassContainer
|
||||
= Jenkins.getInstance().getExtensionList(BlueExtensionClassContainer.class).get(0);
|
||||
Map classInfo = (Map)mergeObjects(extensionClassContainer.get(type));
|
||||
List classInfoClasses = (List)classInfo.get("_classes");
|
||||
Map classInfo = (Map) mergeObjects(extensionClassContainer.get(type));
|
||||
List classInfoClasses = (List) classInfo.get("_classes");
|
||||
classInfoClasses.add(0, type);
|
||||
extension.put("_class", type);
|
||||
extension.put("_classes", classInfoClasses);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("An error occurred when attempting to read type information from jenkins-js-extension.json from: " + dataRes, e);
|
||||
}
|
||||
}
|
||||
String pluginId = getGav(extensionData);
|
||||
if (pluginId != null) {
|
||||
jsExtensionCache.put(pluginId, mergeObjects(extensionData));
|
||||
} else {
|
||||
LOGGER.error(String.format("Plugin %s JS extension has missing hpiPluginId", pluginWrapper.getLongName()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Error reading 'jenkins-js-extension.json' from '" + dataRes + "'. Extensions defined in the host plugin will not be active.", e);
|
||||
|
||||
extensionData.put(PLUGIN_VER, pluginWrapper.getVersion());
|
||||
jsExtensionCache.put(pluginId, mergeObjects(extensionData));
|
||||
hasDefinedExtensions = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasDefinedExtensions) {
|
||||
// Manufacture an entry for all plugins that do not have any defined
|
||||
// extensions. This adds some info about the plugin that the UI might
|
||||
// need access to e.g. the plugin version.
|
||||
Map<String, Object> extensionData = new LinkedHashMap<>();
|
||||
extensionData.put(PLUGIN_ID, pluginWrapper.getShortName());
|
||||
extensionData.put(PLUGIN_VER, pluginWrapper.getVersion());
|
||||
extensionData.put(PLUGIN_EXT, Collections.emptyList());
|
||||
jsExtensionCache.put(pluginWrapper.getShortName(), mergeObjects(extensionData));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.error(String.format("Error locating jenkins-js-extension.json for plugin %s", pluginWrapper.getLongName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// ***********************************************************************************************************
|
||||
// TODO: Someone needs to write some docs on this function, explaining what it is doing and why it's needed.
|
||||
// ***********************************************************************************************************
|
||||
//
|
||||
private static Object mergeObjects(Object incoming) {
|
||||
if (incoming instanceof Map) {
|
||||
Map m = new HashMap();
|
|
@ -0,0 +1,27 @@
|
|||
package io.jenkins.blueocean.config;
|
||||
|
||||
import hudson.Extension;
|
||||
import io.jenkins.blueocean.commons.PageStatePreloader;
|
||||
|
||||
/**
|
||||
* {@link PageStatePreloader} for js-extensions data.
|
||||
*/
|
||||
@Extension
|
||||
public class JenkinsJSExtensionsStatePreloader extends PageStatePreloader {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getStatePropertyPath() {
|
||||
return "jsExtensions";
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getStateJson() {
|
||||
return JenkinsJSExtensions.getExtensionsData().toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package io.jenkins.blueocean.config;
|
||||
|
||||
import hudson.Extension;
|
||||
import io.jenkins.blueocean.BluePageDecorator;
|
||||
import io.jenkins.blueocean.commons.BlueOceanConfigProperties;
|
||||
|
||||
/**
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
@Extension(ordinal = 10)
|
||||
public class RollbarDecorator extends BluePageDecorator {
|
||||
|
||||
public boolean isRollBarEnabled(){
|
||||
return BlueOceanConfigProperties.ROLLBAR_ENABLED;
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ var transformer = function (payload) {
|
|||
var _rollbarConfig = {
|
||||
accessToken: '81f3134dedf44871b9cc0a347b1313df',
|
||||
captureUncaught: true,
|
||||
code_version: window.$blueocean_pluginVersion, // see header.jelly
|
||||
code_version: window.$blueocean.config.version, // see header.jelly
|
||||
source_map_enabled: true,
|
||||
guess_uncaught_frames: true,
|
||||
transform: transformer
|
|
@ -0,0 +1,12 @@
|
|||
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
|
||||
<j:if test="${it.rollBarEnabled}">
|
||||
<!--
|
||||
Running the plugin build (or just "gulp" from the command line) will
|
||||
generate a browser bundle of what's in src/main/js/rollbar.js and we
|
||||
can load that using an adjunct as follows.
|
||||
|
||||
See gulpfile.js and see the output from running the "gulp" command.
|
||||
-->
|
||||
<st:adjunct includes="org.jenkins.ui.jsmodules.blueocean_config.rollbar"/>
|
||||
</j:if>
|
||||
</j:jelly>
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"presets": [
|
||||
"es2015", "react", "stage-0"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-decorators-legacy"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
max_line_length = 160
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "@jenkins-cd/jenkins/react",
|
||||
"rules": {
|
||||
"react/jsx-no-bind": 0,
|
||||
"no-unused-vars": [2, {"varsIgnorePattern": "^React$"}],
|
||||
"max-len": [1, 160, 4],
|
||||
"experimentalDecorators": 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
node_modules/
|
||||
npm-debug.log
|
||||
.DS_Store
|
||||
dist
|
||||
*.iml
|
||||
.idea/
|
||||
/.project
|
||||
reports
|
|
@ -0,0 +1,3 @@
|
|||
# Blue Ocean Core JS
|
||||
|
||||
This is an npm module that contains common javascript libraries/utilities that are used across modules and plugins and blue oceans.
|
|
@ -0,0 +1,135 @@
|
|||
"use strict";
|
||||
|
||||
/*
|
||||
Build file for Jenkins Blue Ocean Commons JavaScript.
|
||||
*/
|
||||
const gulp = require('gulp');
|
||||
const gutil = require('gulp-util');
|
||||
const sourcemaps = require('gulp-sourcemaps');
|
||||
const babel = require('gulp-babel');
|
||||
const less = require('gulp-less');
|
||||
const rename = require('gulp-rename');
|
||||
const copy = require('gulp-copy');
|
||||
const del = require('del');
|
||||
const runSequence = require('run-sequence');
|
||||
const lint = require('gulp-eslint');
|
||||
const Karma = require('karma').Server;
|
||||
const fs = require('fs');
|
||||
|
||||
// Options, src/dest folders, etc
|
||||
|
||||
const config = {
|
||||
clean: ["dist", "licenses", "reports"],
|
||||
react: {
|
||||
sources: "src/**/*.{js,jsx}",
|
||||
dest: "dist"
|
||||
},
|
||||
less: {
|
||||
sources: "src/less/core.less",
|
||||
watch: 'src/less/**/*.{less,css}',
|
||||
dest: "dist/assets/css",
|
||||
},
|
||||
copy: {
|
||||
less_assets: {
|
||||
sources: "src/less/**/*.svg",
|
||||
dest: "dist/assets/css"
|
||||
}
|
||||
},
|
||||
test: {
|
||||
sources: "test/**/*-spec.{js,jsx}"
|
||||
}
|
||||
};
|
||||
|
||||
// Watch all
|
||||
|
||||
gulp.task("watch", ["clean-build"], () => {
|
||||
gulp.watch(config.react.sources, ["compile-react"]);
|
||||
gulp.watch(config.less.watch, ["less"]);
|
||||
});
|
||||
|
||||
// Default to all
|
||||
|
||||
gulp.task("default", () =>
|
||||
runSequence("clean", "lint", "test", "build", "validate"));
|
||||
|
||||
// Clean and build only, for watching
|
||||
|
||||
gulp.task("clean-build", () =>
|
||||
runSequence("clean", "build", "validate"));
|
||||
|
||||
// Clean
|
||||
|
||||
gulp.task("clean", () =>
|
||||
del(config.clean));
|
||||
|
||||
// Testing
|
||||
|
||||
gulp.task("lint", () => (
|
||||
gulp.src([config.react.sources, config.test.sources])
|
||||
.pipe(lint())
|
||||
.pipe(lint.format())
|
||||
.pipe(lint.failAfterError())
|
||||
));
|
||||
|
||||
gulp.task("test", ['test-karma']);
|
||||
|
||||
gulp.task("test-debug", ['test-karma-debug']);
|
||||
|
||||
gulp.task("test-karma", (done) => {
|
||||
new Karma({
|
||||
configFile: __dirname + '/karma.conf.js',
|
||||
}, done).start();
|
||||
});
|
||||
|
||||
gulp.task("test-karma-debug", (done) => {
|
||||
new Karma({
|
||||
configFile: __dirname + '/karma.conf.js',
|
||||
colors: true,
|
||||
autoWatch: true,
|
||||
singleRun: false,
|
||||
browsers: ['Chrome'],
|
||||
}, done).start();
|
||||
});
|
||||
|
||||
// Build all
|
||||
|
||||
gulp.task("build", ["compile-react", "less", "copy"]);
|
||||
|
||||
// Compile react sources
|
||||
|
||||
gulp.task("compile-react", () =>
|
||||
gulp.src(config.react.sources)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(babel(config.react.babel))
|
||||
.pipe(sourcemaps.write("."))
|
||||
.pipe(gulp.dest(config.react.dest)));
|
||||
|
||||
gulp.task("less", () =>
|
||||
gulp.src(config.less.sources)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(less())
|
||||
.pipe(rename("blueocean-core-js.css"))
|
||||
.pipe(sourcemaps.write("."))
|
||||
.pipe(gulp.dest(config.less.dest)));
|
||||
|
||||
gulp.task("copy", ["copy-less-assets"]);
|
||||
|
||||
gulp.task("copy-less-assets", () =>
|
||||
gulp.src(config.copy.less_assets.sources)
|
||||
.pipe(copy(config.copy.less_assets.dest, { prefix: 2 })));
|
||||
|
||||
// Validate contents
|
||||
gulp.task("validate", () => {
|
||||
const paths = [
|
||||
config.react.dest,
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
fs.statSync(path);
|
||||
} catch (err) {
|
||||
gutil.log('Error occurred during validation; see stack trace for details');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
// Karma configuration
|
||||
// Generated on Wed Jun 01 2016 16:04:37 GMT-0400 (EDT)
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
|
||||
basePath: '',
|
||||
|
||||
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
||||
frameworks: ['mocha', 'browserify'],
|
||||
|
||||
junitReporter: {
|
||||
outputDir: 'reports'
|
||||
},
|
||||
|
||||
// include only tests here; browserify will find the rest
|
||||
files: [
|
||||
'test/js/test-entrypoint.js',
|
||||
'test/**/*-spec.+(js|jsx)'
|
||||
],
|
||||
|
||||
exclude: [],
|
||||
|
||||
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
|
||||
preprocessors: {
|
||||
'test/js/test-entrypoint.js': ['browserify'],
|
||||
'test/**/*-spec.+(js|jsx)': ['browserify']
|
||||
},
|
||||
|
||||
browserify: {
|
||||
debug: true,
|
||||
transform: ['babelify'],
|
||||
extensions: ['.js', '.jsx'],
|
||||
// needed for enzyme
|
||||
configure: function (bundle) {
|
||||
bundle.on('prebundle', function () {
|
||||
bundle.external('react/addons');
|
||||
bundle.external('react/lib/ReactContext');
|
||||
bundle.external('react/lib/ExecutionEnvironment');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||
reporters: ['mocha','junit'],
|
||||
|
||||
// web server port
|
||||
port: 9876,
|
||||
|
||||
// enable / disable colors in the output (reporters and logs)
|
||||
colors: false,
|
||||
|
||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||
logLevel: config.LOG_INFO,
|
||||
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
autoWatch: false,
|
||||
|
||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
browsers: ['PhantomJS'],
|
||||
|
||||
// if true, Karma captures browsers, runs the tests and exits
|
||||
singleRun: true,
|
||||
|
||||
// how many browser should be started simultaneous
|
||||
concurrency: Infinity
|
||||
})
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,86 @@
|
|||
{
|
||||
"name": "@jenkins-cd/blueocean-core-js",
|
||||
"version": "0.0.44-unpublished",
|
||||
"description": "Shared JavaScript libraries for use with Jenkins Blue Ocean",
|
||||
"main": "dist/js/index.js",
|
||||
"scripts": {
|
||||
"gulp": "gulp",
|
||||
"test": "gulp test",
|
||||
"prepublish": "gulp"
|
||||
},
|
||||
"author": "Cliff Meyers <cmeyers@cloudbees.com> (https://www.cloudbees.com/)",
|
||||
"contributors": [
|
||||
"Cliff Meyers <cliff.meyers@gmail.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"licenses",
|
||||
"dist"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jenkinsci/blueocean-plugin.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jenkins-cd/js-extensions": "0.0.32",
|
||||
"@jenkins-cd/js-modules": "0.0.8",
|
||||
"@jenkins-cd/react-material-icons": "1.0.0",
|
||||
"@jenkins-cd/sse-gateway": "0.0.10",
|
||||
"@jenkins-cd/storage": "0.0.3",
|
||||
"es6-promise": "4.0.5",
|
||||
"i18next": "3.5.2",
|
||||
"i18next-browser-languagedetector": "1.0.0",
|
||||
"i18next-xhr-backend": "1.2.0",
|
||||
"isomorphic-fetch": "2.2.1",
|
||||
"jsonwebtoken": "7.1.9",
|
||||
"mobx": "2.6.0",
|
||||
"mobx-utils": "1.1.2",
|
||||
"pem-jwk": "1.5.1",
|
||||
"react-router": "3.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^15.1.0",
|
||||
"react-dom": "^15.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jenkins-cd/eslint-config-jenkins": "0.0.2",
|
||||
"babel-eslint": "7.0.0",
|
||||
"babel-plugin-transform-decorators-legacy": "1.3.4",
|
||||
"babel-polyfill": "6.16.0",
|
||||
"babel-preset-es2015": "6.16.0",
|
||||
"babel-preset-react": "6.16.0",
|
||||
"babel-preset-stage-0": "6.16.0",
|
||||
"babelify": "7.3.0",
|
||||
"browserify": "13.1.0",
|
||||
"chai": "3.5.0",
|
||||
"del": "2.2.2",
|
||||
"enzyme": "2.4.1",
|
||||
"eslint": "2.13.1",
|
||||
"eslint-plugin-react": "4.3.0",
|
||||
"flow-bin": "0.34.0",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-babel": "6.1.2",
|
||||
"gulp-copy": "0.0.2",
|
||||
"gulp-eslint": "3.0.1",
|
||||
"gulp-less": "3.1.0",
|
||||
"gulp-rename": "1.2.2",
|
||||
"gulp-sourcemaps": "1.6.0",
|
||||
"karma": "1.3.0",
|
||||
"karma-browserify": "5.1.0",
|
||||
"karma-chrome-launcher": "2.0.0",
|
||||
"karma-junit-reporter": "1.1.0",
|
||||
"karma-mocha": "1.2.0",
|
||||
"karma-mocha-reporter": "2.2.0",
|
||||
"karma-phantomjs-launcher": "1.0.2",
|
||||
"mocha": "3.1.0",
|
||||
"phantomjs-prebuilt": "2.1.12",
|
||||
"react": "15.3.2",
|
||||
"react-addons-test-utils": "15.3.2",
|
||||
"react-dom": "15.3.2",
|
||||
"run-sequence": "1.2.2",
|
||||
"sinon": "1.17.6",
|
||||
"watchify": "3.7.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
import Fullscreen from './Fullscreen';
|
||||
import { Link } from 'react-router';
|
||||
import i18nTranslator from './i18n/i18n';
|
||||
|
||||
const translate = i18nTranslator('blueocean-web');
|
||||
|
||||
/**
|
||||
* Simple component to render a fullscreen 404 page
|
||||
*/
|
||||
export default () => (
|
||||
<Fullscreen className="not-found">
|
||||
<div className="message-box">
|
||||
<h3>{translate('Not.found.heading', {
|
||||
defaultValue: 'Page not found (404)',
|
||||
})}</h3>
|
||||
<div className="message">{translate('Not.found.message', { defaultValue: 'Jenkins could not find the page you were looking for. Check the URL for errors or press the back button.' })}</div>
|
||||
<div className="actions"><Link to="/" className="btn">{translate('Open.dashboard', { defaultValue: 'Open Dashboard' })}</Link></div>
|
||||
</div>
|
||||
</Fullscreen>
|
||||
);
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* Created by cmeyers on 8/18/16.
|
||||
*/
|
||||
import { action, observable, computed } from 'mobx';
|
||||
|
||||
/**
|
||||
* Holds one or more toasts in state for display in UI.
|
||||
*/
|
||||
export class ToastService {
|
||||
|
||||
@observable toasts = [];
|
||||
|
||||
/**
|
||||
* Creates a new toast that is added to the list.
|
||||
*
|
||||
* @param toast object with the following shape:
|
||||
* {
|
||||
* text: string, message text to display
|
||||
* action: string, text for action link
|
||||
* onActionClick: function, callback to invoke when action link is clicked
|
||||
* onDismiss: function, callback to invoke when toast is dismissed (immediately, or after timeout)
|
||||
* dismissDelay: number, duration in millis after which to auto-dismiss this Toast
|
||||
* id: unique identifier (optional, will be autogenerated if ommitted)
|
||||
* }
|
||||
* @returns {number} unique ID of toast
|
||||
*/
|
||||
@action
|
||||
newToast(toast) {
|
||||
// prevent duplicate toasts from appearing when multiple UI elements
|
||||
// are listening for an event that triggers creation of a toast
|
||||
if (this._hasDuplicate(toast)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const newToast = toast;
|
||||
|
||||
if (!newToast.id) {
|
||||
newToast.id = Math.random() * Math.pow(10, 16);
|
||||
}
|
||||
|
||||
this.toasts.push(newToast);
|
||||
|
||||
return newToast.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a toast with the matching value of toast.id.
|
||||
*
|
||||
* @param toast
|
||||
*/
|
||||
@action
|
||||
removeToast(toast) {
|
||||
this.toasts = this.toasts.filter((item) =>
|
||||
toast.id !== item.id
|
||||
);
|
||||
}
|
||||
|
||||
@computed
|
||||
get count() {
|
||||
return this.toasts ? this.toasts.length : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a toast with the same 'text' and 'action' already exists
|
||||
* @param newToast
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
_hasDuplicate(newToast) {
|
||||
for (const toast of this.toasts) {
|
||||
if (toast.text === newToast.text &&
|
||||
toast.action === newToast.action) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* Created by cmeyers on 9/21/16.
|
||||
*/
|
||||
|
||||
import { ToastService as toastService } from './index';
|
||||
import { buildRunDetailsUrlFromQueue } from './UrlBuilder';
|
||||
import i18nTranslator from './i18n/i18n';
|
||||
|
||||
const CAPABILITY_MULTIBRANCH_PIPELINE = 'io.jenkins.blueocean.rest.model.BlueMultiBranchPipeline';
|
||||
const CAPABILITY_MULTIBRANCH_BRANCH = 'io.jenkins.blueocean.rest.model.BlueBranch';
|
||||
|
||||
export default {
|
||||
/**
|
||||
*
|
||||
* @param runnable
|
||||
* @param runInfo
|
||||
* @param toastAction
|
||||
*/
|
||||
createRunStartedToast: (runnable, runInfo, toastAction) => {
|
||||
const translate = i18nTranslator('blueocean-web');
|
||||
const isMultiBranch = runnable._capabilities.some(capability => (
|
||||
[CAPABILITY_MULTIBRANCH_PIPELINE, CAPABILITY_MULTIBRANCH_BRANCH].indexOf(capability) !== -1
|
||||
));
|
||||
|
||||
const runId = runInfo.expectedBuildNumber;
|
||||
|
||||
const runDetailsUrl = buildRunDetailsUrlFromQueue(
|
||||
runInfo._links.self.href,
|
||||
isMultiBranch,
|
||||
runId,
|
||||
);
|
||||
|
||||
const name = decodeURIComponent(runnable.name);
|
||||
const text = translate('toast.run.started', {
|
||||
0: name,
|
||||
1: runId,
|
||||
defaultValue: 'Started "{0}" #{1}',
|
||||
});
|
||||
|
||||
const caption = translate('toast.run.open', {
|
||||
defaultValue: 'Open',
|
||||
});
|
||||
toastService.newToast({
|
||||
text,
|
||||
action: caption,
|
||||
onActionClick: () => {
|
||||
if (toastAction) {
|
||||
toastAction(runDetailsUrl);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return runDetailsUrl;
|
||||
},
|
||||
};
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* Created by cmeyers on 8/25/16.
|
||||
*/
|
||||
|
||||
const extractRestUrl = (subject) => {
|
||||
let restUrl = null;
|
||||
|
||||
if (typeof subject === 'object') {
|
||||
if (subject && subject._links && subject._links.self) {
|
||||
restUrl = subject._links.self.href;
|
||||
}
|
||||
} else if (typeof subject === 'string') {
|
||||
restUrl = subject;
|
||||
}
|
||||
|
||||
if (!restUrl) {
|
||||
throw new Error('Could not find input URL');
|
||||
}
|
||||
|
||||
return restUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a new array with leading and trailing whitespace elements removed.
|
||||
*
|
||||
* @param {Array} tokens
|
||||
* @returns {Array}
|
||||
*/
|
||||
const trimEmptyTokens = (tokens) => {
|
||||
const copy = tokens.slice();
|
||||
|
||||
if (copy[0] === '') {
|
||||
copy.shift();
|
||||
}
|
||||
|
||||
if (copy[copy.length - 1] === '') {
|
||||
copy.pop();
|
||||
}
|
||||
|
||||
return copy;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the proper URL to view Run Details for the specified run.
|
||||
* Run is either a run object with "_links.self.href" property, or the URL itself.
|
||||
*
|
||||
* @param {object|string} run
|
||||
* @returns {string}
|
||||
*/
|
||||
export const buildRunDetailsUrl = (run) => {
|
||||
const restUrl = extractRestUrl(run);
|
||||
const tokens = trimEmptyTokens(restUrl.split('/'));
|
||||
|
||||
// given the following URL '/blue/rest/organizations/jenkins/pipelines/folder1/pipelines/folder2/pipelines/folder3/pipelines/jdl-2
|
||||
// /branches/experiment%252Fbuild-locally-docker/runs/21/
|
||||
|
||||
const organizationName = tokens[3];
|
||||
const isMultiBranch = tokens[tokens.length - 4] === 'branches';
|
||||
|
||||
const fullNameStart = 4;
|
||||
const fullNameEnd = !isMultiBranch ? tokens.length - 2 : tokens.length - 4;
|
||||
// grab the tokens that make up the full name, then filter out the even values ('/pipelines')
|
||||
// so the clean folder path is returned, e.g. folder1/folder2/folder3/jdl-2
|
||||
const fullName = tokens
|
||||
.slice(fullNameStart, fullNameEnd)
|
||||
.filter((name, index) => index % 2 === 1)
|
||||
.join('/');
|
||||
|
||||
const pipelineName = tokens[fullNameEnd - 1];
|
||||
const branchName = isMultiBranch ? tokens[tokens.length - 3] : '';
|
||||
const runId = tokens[tokens.length - 1];
|
||||
|
||||
const detailName = isMultiBranch ? decodeURIComponent(branchName) : pipelineName;
|
||||
|
||||
// fail fast
|
||||
if (!organizationName || !fullName || !detailName || !runId) {
|
||||
throw new Error('Could not extract URI components');
|
||||
}
|
||||
|
||||
return `/organizations/${organizationName}` +
|
||||
`/${encodeURIComponent(fullName)}/detail` +
|
||||
`/${detailName}/${runId}/pipeline`;
|
||||
};
|
||||
|
||||
export const buildRunDetailsUrlFromQueue = (queueItem, isMultiBranch, expectedBuildNumber) => {
|
||||
const restUrl = extractRestUrl(queueItem);
|
||||
const tokens = trimEmptyTokens(restUrl.split('/'));
|
||||
|
||||
// given the following URL '/blue/rest/organizations/jenkins/pipelines/jenkinsfile-experiments/pipelines/PR-2/queue/31/'
|
||||
|
||||
// modify the 'queue' URL so it looks like a 'runs' URL
|
||||
tokens[tokens.length - 2] = 'runs';
|
||||
|
||||
// replace the queue number with the expected runId
|
||||
tokens[tokens.length - 1] = expectedBuildNumber;
|
||||
|
||||
// if multi-branch, change the last value of 'pipelines' to 'branches' so it looks like a multibranch REST URL
|
||||
if (isMultiBranch) {
|
||||
tokens[tokens.length - 4] = 'branches';
|
||||
}
|
||||
|
||||
return buildRunDetailsUrl(tokens.join('/'));
|
||||
};
|
||||
|
||||
export default {
|
||||
buildRunDetailsUrl, buildRunDetailsUrlFromQueue,
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* Created by cmeyers on 7/8/16.
|
||||
*/
|
||||
|
||||
import { blueocean } from './scopes';
|
||||
|
||||
export class User {
|
||||
|
||||
constructor(blueUser = {}) {
|
||||
this._class = blueUser._class;
|
||||
this._links = blueUser._links;
|
||||
this.email = blueUser.email;
|
||||
this.fullName = blueUser.fullName;
|
||||
this.id = blueUser.id || 'anonymous';
|
||||
}
|
||||
|
||||
isAnonymous() {
|
||||
return (this.id === 'anonymous');
|
||||
}
|
||||
|
||||
static current() {
|
||||
return CURRENT; // eslint-disable-line no-use-before-define
|
||||
}
|
||||
}
|
||||
|
||||
let CURRENT = new User(blueocean.user);
|
||||
|
||||
/* eslint-disable */
|
||||
export const TestUtil = {
|
||||
setCurrent: function (user) {
|
||||
CURRENT = new User(user);
|
||||
},
|
||||
};
|
||||
/* eslint-enable */
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Created by cmeyers on 9/8/16.
|
||||
*/
|
||||
import { Fetch } from '../fetch';
|
||||
import config from '../urlconfig';
|
||||
import utils from '../utils';
|
||||
|
||||
export class CapabilityApi {
|
||||
|
||||
/**
|
||||
* Fetch the capabilities for one or more class names.
|
||||
*
|
||||
* @param {Array} classNames
|
||||
* @returns {Promise} with fulfilled {object} keyed by className, with an array of string capability names.
|
||||
* @private
|
||||
*/
|
||||
fetchCapabilities(classNames) {
|
||||
const noDuplicates = classNames.filter((item, index, self) => self.indexOf(item) === index);
|
||||
const path = config.getJenkinsRootURL();
|
||||
const classesUrl = utils.cleanSlashes(`${path}/blue/rest/classes/`);
|
||||
|
||||
const fetchOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(
|
||||
{ q: noDuplicates }
|
||||
),
|
||||
};
|
||||
|
||||
return Fetch.fetchJSON(classesUrl, { disableCapabilites: true, fetchOptions });
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
/**
|
||||
* Created by cmeyers on 9/8/16.
|
||||
*/
|
||||
|
||||
const addClass = (clazz, classMap) => {
|
||||
let className;
|
||||
|
||||
if (Array.isArray(clazz._class)) {
|
||||
// If it's an array of class names, just take the first.
|
||||
// TODO: Hmmm ... not sure if this is the right thing to do when we have an array of class names.
|
||||
// Not sure what the array is about tbh. What are the relationships?
|
||||
if (clazz._class.length > 0) {
|
||||
className = clazz._class[0];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
className = clazz._class;
|
||||
}
|
||||
|
||||
if (!classMap[className]) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
classMap[className] = [];
|
||||
}
|
||||
|
||||
classMap[className].push(clazz);
|
||||
};
|
||||
|
||||
const canWalk = (item) => item && (typeof item === 'object' || Array.isArray(item));
|
||||
|
||||
const DEFAULT_IGNORED_PROPS = ['_links'];
|
||||
|
||||
/**
|
||||
* Decorate an object graph with a '_capabilities' property for each object with a valid '_class'
|
||||
* Usage:
|
||||
* import { capabilityAugmenter } from '@jenkins-cd/blueocean-core-js';
|
||||
* const augmentCapability = capabilityAugmenter.augmentCapability;
|
||||
*
|
||||
* fetch(url, fetchOptions)
|
||||
* .then(data => augmentCapability(data));
|
||||
*/
|
||||
export class CapabilityAugmenter {
|
||||
|
||||
constructor(capabilityStore) {
|
||||
this._capabilityStore = capabilityStore;
|
||||
this._perfLoggingEnabled = false;
|
||||
this._warnLoggingEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "_capabilities" data or all objects with a "_class" property.
|
||||
*
|
||||
* @param {object|Array} data
|
||||
* @returns {Promise}
|
||||
*/
|
||||
augmentCapabilities(data) {
|
||||
const classMap = this._findClassesInTree(data);
|
||||
return this._resolveCapabilities(data, classMap);
|
||||
}
|
||||
|
||||
enablePerfLogging() {
|
||||
this._perfLoggingEnabled = true;
|
||||
}
|
||||
|
||||
enableWarningLogging() {
|
||||
this._warnLoggingEnabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all of the distinct "_class" values in supplied object.
|
||||
*
|
||||
* @param {object|Array} data
|
||||
* @returns {object} key= "_class" name, value= array of all objects of that class.
|
||||
* @private
|
||||
*/
|
||||
_findClassesInTree(data) {
|
||||
const classMap = {};
|
||||
const nodesToWalk = [data];
|
||||
const nodesAlreadyWalked = [];
|
||||
const ignoredProps = DEFAULT_IGNORED_PROPS.slice();
|
||||
|
||||
const started = new Date().getTime();
|
||||
|
||||
let node = nodesToWalk.shift();
|
||||
|
||||
while (node) {
|
||||
nodesAlreadyWalked.push(node);
|
||||
|
||||
// save a ref to the class so we can attach capabilities later
|
||||
if (typeof node === 'object' && node._class) {
|
||||
addClass(node, classMap);
|
||||
}
|
||||
|
||||
const nodeKeys = Object.keys(node);
|
||||
|
||||
for (const key of nodeKeys) {
|
||||
const value = node[key];
|
||||
|
||||
// walk this node at a later iteration as long as
|
||||
// - we didn't already walk it (cycles cause an infinite loop otherwise)
|
||||
// - the property name isn't on the blacklist
|
||||
if (canWalk(value) && nodesAlreadyWalked.indexOf(value) === -1 && ignoredProps.indexOf(key) === -1) {
|
||||
nodesToWalk.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
node = nodesToWalk.shift();
|
||||
}
|
||||
|
||||
if (this._perfLoggingEnabled) {
|
||||
console.debug(`augmenter.parse: ${new Date().getTime() - started}ms`);
|
||||
}
|
||||
|
||||
return classMap;
|
||||
}
|
||||
|
||||
_resolveCapabilities(data, classMap) {
|
||||
const classNames = Object.keys(classMap);
|
||||
|
||||
return this._capabilityStore.resolveCapabilities(...classNames)
|
||||
.then(capabilitiesMap => this._injectCapabilities(classMap, capabilitiesMap))
|
||||
.then(() => data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the capabilities to the "_capabilities" property of all objects in the class map.
|
||||
*
|
||||
* @param classMap
|
||||
* @param capabilitiesMap
|
||||
* @returns {object} classMap
|
||||
* @private
|
||||
*/
|
||||
_injectCapabilities(classMap, capabilitiesMap) {
|
||||
const started = new Date().getTime();
|
||||
const unresolved = [];
|
||||
|
||||
for (const className of Object.keys(classMap)) {
|
||||
for (const target of classMap[className]) {
|
||||
const capabilities = capabilitiesMap[className];
|
||||
|
||||
if (!capabilities && unresolved.indexOf(className) === -1) {
|
||||
unresolved.push(className);
|
||||
}
|
||||
|
||||
target._capabilities = capabilities || [];
|
||||
}
|
||||
}
|
||||
|
||||
if (this._perfLoggingEnabled) {
|
||||
console.debug(`augmenter.inject: ${new Date().getTime() - started}ms`);
|
||||
}
|
||||
|
||||
if (this._warnLoggingEnabled) {
|
||||
for (const className of unresolved) {
|
||||
console.warn(`could not resolve capabilities for ${className}; an error may have occurred during lookup`);
|
||||
}
|
||||
}
|
||||
|
||||
return classMap;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
/**
|
||||
* Created by cmeyers on 8/31/16.
|
||||
*/
|
||||
import es6Promise from 'es6-promise'; es6Promise.polyfill();
|
||||
import { installInfo } from '../storage';
|
||||
|
||||
// Create a dedicated storage namespace that we use to store classes
|
||||
// info in the browser, eliminating client REST call overhead for classes
|
||||
// info. This storage namespace will be auto-cleared if the jesnkins version
|
||||
// changes, or if the active plugins change.
|
||||
const classesInfoNS = installInfo.subspace('classesInfo');
|
||||
|
||||
/**
|
||||
* Retrieves capability metadata for class names.
|
||||
* Uses an internal cache to minimize REST API calls.
|
||||
*/
|
||||
export class CapabilityStore {
|
||||
|
||||
constructor(capabilityApi) {
|
||||
this._localStore = {};
|
||||
this._capabilityApi = capabilityApi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the capabilities for one or more class names.
|
||||
* Will used cached values if available.
|
||||
*
|
||||
* @param classNames
|
||||
* @returns {Promise} with fulfilled {object} keyed by className, with an array of string capability names.
|
||||
*/
|
||||
resolveCapabilities(...classNames) {
|
||||
const result = {};
|
||||
const classesToFetch = [];
|
||||
|
||||
// determine which class names are already in the cache and which aren't
|
||||
for (const className of classNames) {
|
||||
const classInfo = this._getStoredClassInfo(className);
|
||||
if (classInfo) {
|
||||
result[className] = classInfo;
|
||||
} else {
|
||||
classesToFetch.push(className);
|
||||
}
|
||||
}
|
||||
|
||||
// if nothing to fetch, just return an immediately fulfilled Promise
|
||||
if (classesToFetch.length === 0) {
|
||||
return new Promise(resolve => resolve(result));
|
||||
}
|
||||
|
||||
// fetch the capabilities and then merge that with the values already in the cache
|
||||
return this._fetchCapabilities(classesToFetch)
|
||||
.then(fetchedCapabilities => Object.assign(result, fetchedCapabilities));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the capabilities for one or more class names.
|
||||
*
|
||||
* @param classNames
|
||||
* @returns {Promise} with fulfilled {object} keyed by className, with an array of string capability names.
|
||||
* @private
|
||||
*/
|
||||
_fetchCapabilities(classNames) {
|
||||
return this._capabilityApi.fetchCapabilities(classNames)
|
||||
.then(data => this._storeCapabilities(data.map));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the values in the cache and return it.
|
||||
*
|
||||
* @param map
|
||||
* @returns {object} keyed by className, with an array of string capability names.
|
||||
* @private
|
||||
*/
|
||||
_storeCapabilities(map) {
|
||||
const storedCapabilities = {};
|
||||
|
||||
Object.keys(map).forEach(className => {
|
||||
const capabilities = map[className];
|
||||
this._localStore[className] = storedCapabilities[className] = capabilities.classes.slice();
|
||||
// Also store in the browser so we don't have to look
|
||||
// up this info again (unless the storage namespace is
|
||||
// cleared due to jenkins or plugin changes).
|
||||
classesInfoNS.set(className, this._localStore[className]);
|
||||
});
|
||||
|
||||
return storedCapabilities;
|
||||
}
|
||||
|
||||
_getStoredClassInfo(className) {
|
||||
if (!this._localStore[className]) {
|
||||
// If we don't have a copy of the class info in the localStore,
|
||||
// check the browser storage and copy it into the localStore.
|
||||
// We still want to use the localStore because it holds deserialized
|
||||
// copies of the class info, which means that a localStore lookup
|
||||
// would be lower overhead and probably faster.
|
||||
this._localStore[className] = classesInfoNS.get(className);
|
||||
}
|
||||
return this._localStore[className];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* Created by cmeyers on 9/9/16.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Determines whether the supplied object has at least one of the supplied capabilities.
|
||||
*
|
||||
* As capabilities are typically name-spaced, this method will match on long or short names, e.g.
|
||||
* given: _capabilities = ['a.b.Capability1']
|
||||
* passing either 'a.b.Capability1' or 'Capability1' will match
|
||||
*
|
||||
* @param {object} subject
|
||||
* @param {...string} capabilityNames
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const capable = (subject, ...capabilityNames) => {
|
||||
if (subject && subject._capabilities) {
|
||||
// in case an array was passed in, flatten it out
|
||||
const flattenedCapabilities = [].concat(...capabilityNames);
|
||||
|
||||
// find the intersection of subject's caps with the passed-in caps
|
||||
const longNameMatches = flattenedCapabilities.filter(longName => subject._capabilities.indexOf(longName) !== -1);
|
||||
if (longNameMatches.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// build short form of subject's caps, then find intersection
|
||||
const shortNames = subject._capabilities.map(longName => longName.split('.').slice(-1).join(''));
|
||||
|
||||
const shortNameMatches = flattenedCapabilities.filter(longName => shortNames.indexOf(longName) !== -1);
|
||||
if (shortNameMatches.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides some convenience methods for host object of _class/_capabilities
|
||||
*/
|
||||
export class Capable {
|
||||
/**
|
||||
* Determines whether the host object has the supplied capability.
|
||||
* As capabilities are typically name-spaced, this method will match on long or short names, e.g.
|
||||
* if _capabilities = ['a.b.Capability1']
|
||||
* passing either 'a.b.Capability1' or 'Capability1' will match
|
||||
*
|
||||
* @param {string} capabilityName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
can(capabilityName) {
|
||||
return capable(this, capabilityName);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Capable();
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Created by cmeyers on 9/8/16.
|
||||
*/
|
||||
import { CapabilityApi } from './CapabilityApi';
|
||||
import { CapabilityStore } from './CapabilityStore';
|
||||
import { CapabilityAugmenter } from './CapabilityAugmenter';
|
||||
|
||||
const api = new CapabilityApi();
|
||||
const store = new CapabilityStore(api);
|
||||
const augmenter = new CapabilityAugmenter(store);
|
||||
|
||||
// export as named singletons
|
||||
export { store as capabilityStore };
|
||||
export { augmenter as capabilityAugmenter };
|
||||
export { capable } from './Capable';
|
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* Created by cmeyers on 8/30/16.
|
||||
*/
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { Icon } from '@jenkins-cd/react-material-icons';
|
||||
|
||||
import { capable, RunApi as runApi, ToastUtils } from '../index';
|
||||
import Security from '../security';
|
||||
|
||||
const { permit } = Security;
|
||||
|
||||
const stopProp = (event) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const CAPABILITY_MULTIBRANCH_PIPELINE = 'io.jenkins.blueocean.rest.model.BlueMultiBranchPipeline';
|
||||
const CAPABILITY_MULTIBRANCH_BRANCH = 'io.jenkins.blueocean.rest.model.BlueBranch';
|
||||
const CAPABILITY_SIMPLE_PIPELINE = 'org.jenkinsci.plugins.workflow.job.WorkflowJob';
|
||||
const PIPELINE_CAPABILITIES = [CAPABILITY_SIMPLE_PIPELINE, CAPABILITY_MULTIBRANCH_PIPELINE, CAPABILITY_MULTIBRANCH_BRANCH];
|
||||
|
||||
function isRunFailed(run) {
|
||||
const failureResults = ['FAILURE', 'ABORTED'];
|
||||
return !!(run && run.result && failureResults.indexOf(run.result.toUpperCase()) !== -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* ReplayButton allows a pipeline or branch to be re-run when in a failure state.
|
||||
*/
|
||||
export class ReplayButton extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
replaying: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const statusChanged = isRunFailed(this.props.latestRun) !== isRunFailed(nextProps.latestRun);
|
||||
|
||||
if (statusChanged) {
|
||||
this.setState({
|
||||
replaying: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onReplayClick() {
|
||||
if (this.state.replaying) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
replaying: true,
|
||||
});
|
||||
|
||||
runApi.replayRun(this.props.latestRun)
|
||||
.then(runInfo => ToastUtils.createRunStartedToast(this.props.runnable, runInfo, this.props.onNavigation))
|
||||
.then(runDetailsUrl => this._afterReplayStarted(runDetailsUrl));
|
||||
}
|
||||
|
||||
_afterReplayStarted(runDetailsUrl) {
|
||||
if (this.props.autoNavigate && this.props.onNavigation) {
|
||||
this.props.onNavigation(runDetailsUrl);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.runnable || !this.props.latestRun) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const outerClass = this.props.className ? this.props.className : '';
|
||||
const outerClassNames = outerClass.split(' ');
|
||||
const innerButtonClass = outerClassNames.indexOf('icon-button') === -1 ? 'btn inverse' : '';
|
||||
|
||||
const isFailed = isRunFailed(this.props.latestRun);
|
||||
const isPipeline = capable(this.props.runnable, PIPELINE_CAPABILITIES);
|
||||
const hasPermission = permit(this.props.runnable).start();
|
||||
|
||||
const replayLabel = 'Re-run';
|
||||
|
||||
if (!isFailed || !isPipeline || !hasPermission) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`replay-button-component ${outerClass}`} onClick={(event => stopProp(event))}>
|
||||
<a className={`replay-button ${innerButtonClass}`} title={replayLabel} onClick={() => this._onReplayClick()}>
|
||||
<Icon size={24} icon="replay" />
|
||||
<span className="button-label">{replayLabel}</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReplayButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
runnable: PropTypes.object,
|
||||
latestRun: PropTypes.object,
|
||||
autoNavigate: PropTypes.bool,
|
||||
onNavigation: PropTypes.func,
|
||||
};
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* Created by cmeyers on 8/26/16.
|
||||
*/
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { Icon } from '@jenkins-cd/react-material-icons';
|
||||
import {
|
||||
RunApi as runApi,
|
||||
ToastService as toastService,
|
||||
ToastUtils,
|
||||
} from '../';
|
||||
import Security from '../security';
|
||||
import i18nTranslator from '../i18n/i18n';
|
||||
|
||||
const translate = i18nTranslator('blueocean-web');
|
||||
|
||||
const { permit } = Security;
|
||||
|
||||
const stopProp = (event) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
/**
|
||||
* Run Buttons allows a pipeline or branch to be run and also be stopped thereafter.
|
||||
*/
|
||||
export class RunButton extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
running: false,
|
||||
stopping: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this._updateState(nextProps);
|
||||
}
|
||||
|
||||
_updateState(nextProps) {
|
||||
const oldStatus = this.props.latestRun && this.props.latestRun.state || '';
|
||||
const newStatus = nextProps.latestRun && nextProps.latestRun.state || '';
|
||||
|
||||
// if the state of the run changed, then assume it's no longer trying to stop
|
||||
if (oldStatus !== newStatus) {
|
||||
this.setState({
|
||||
stopping: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onRunClick() {
|
||||
runApi.startRun(this.props.runnable)
|
||||
.then((runInfo) => ToastUtils.createRunStartedToast(this.props.runnable, runInfo, this.props.onNavigation));
|
||||
}
|
||||
|
||||
_onStopClick() {
|
||||
if (this.state.stopping) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
stopping: true,
|
||||
});
|
||||
|
||||
if (this.props.latestRun.state === 'QUEUED') {
|
||||
runApi.removeFromQueue(this.props.latestRun);
|
||||
} else {
|
||||
runApi.stopRun(this.props.latestRun);
|
||||
}
|
||||
|
||||
const name = decodeURIComponent(this.props.runnable.name);
|
||||
const runId = this.props.latestRun.id;
|
||||
const text = translate('toast.run.stopping', {
|
||||
0: name,
|
||||
1: runId,
|
||||
defaultValue: 'Stoppping "{0}" #{1}',
|
||||
});
|
||||
|
||||
toastService.newToast({ text });
|
||||
}
|
||||
|
||||
render() {
|
||||
const outerClass = this.props.className ? this.props.className : '';
|
||||
const outerClassNames = outerClass.split(' ');
|
||||
const innerButtonClass = outerClassNames.indexOf('icon-button') === -1 ? this.props.innerButtonClasses : '';
|
||||
const stopClass = this.state.stopping ? 'stopping' : '';
|
||||
|
||||
const status = this.props.latestRun ? this.props.latestRun.state : '';
|
||||
const isPaused = status.toLowerCase() === 'paused';
|
||||
const runningStatus = status && (isPaused || status.toLowerCase() === 'running' || status.toLowerCase() === 'queued');
|
||||
|
||||
let showRunButton = this.props.buttonType === 'run-only' || (this.props.buttonType === 'toggle' && !runningStatus);
|
||||
let showStopButton = runningStatus && (this.props.buttonType === 'toggle' || this.props.buttonType === 'stop-only');
|
||||
|
||||
showRunButton = showRunButton && permit(this.props.runnable).start();
|
||||
showStopButton = showStopButton && permit(this.props.runnable).stop();
|
||||
|
||||
const runLabel = this.props.runText || translate('toast.run', {
|
||||
defaultValue: 'Run',
|
||||
});
|
||||
let stopLabel = this.state.stopping ? translate('toast.stopping', {
|
||||
defaultValue: 'Stopping ...',
|
||||
}) : translate('toast.stop', {
|
||||
defaultValue: 'Stop',
|
||||
});
|
||||
|
||||
if (isPaused && !this.state.stopping) {
|
||||
stopLabel = translate('toast.abort', { defaultValue: 'Abort' });
|
||||
}
|
||||
|
||||
if (!showRunButton && !showStopButton) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`run-button-component ${outerClass}`} onClick={(event => stopProp(event))}>
|
||||
{ showRunButton &&
|
||||
<a className={`run-button ${innerButtonClass}`} title={runLabel} onClick={() => this._onRunClick()}>
|
||||
<Icon size={24} icon="play_circle_outline" />
|
||||
<span className="button-label">{runLabel}</span>
|
||||
</a>
|
||||
}
|
||||
|
||||
{ showStopButton &&
|
||||
<a className={`stop-button ${innerButtonClass} ${stopClass}`} title={stopLabel} onClick={() => this._onStopClick()}>
|
||||
{ /* eslint-disable max-len */ }
|
||||
<svg className="svg-icon" width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M-2-2h24v24H-2z" />
|
||||
<path className="svg-icon-inner" d="M10 0C4.48 0 0 4.48 0 10s4.48 10 10 10 10-4.48 10-10S15.52 0 10 0zM7 7h6v6H7V7zm3 11c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" fill="#4A90E2" />
|
||||
</g>
|
||||
</svg>
|
||||
{ /* eslint-enable max-len */ }
|
||||
<span className="button-label">{stopLabel}</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RunButton.propTypes = {
|
||||
buttonType: PropTypes.oneOf('toggle', 'stop-only', 'run-only'),
|
||||
className: PropTypes.string,
|
||||
runnable: PropTypes.object,
|
||||
latestRun: PropTypes.object,
|
||||
onNavigation: PropTypes.func,
|
||||
runText: PropTypes.string,
|
||||
innerButtonClasses: PropTypes.string,
|
||||
};
|
||||
|
||||
RunButton.defaultProps = {
|
||||
buttonType: 'toggle',
|
||||
innerButtonClasses: 'btn inverse',
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* This config object comes from blueocean-config.
|
||||
*/
|
||||
import { blueocean } from './scopes';
|
||||
|
||||
const config = blueocean.config || {};
|
||||
|
||||
export default {
|
||||
getJenkinsConfig() {
|
||||
return config.jenkinsConfig || {};
|
||||
},
|
||||
|
||||
getSecurityConfig() {
|
||||
return this.getJenkinsConfig().security || {};
|
||||
},
|
||||
|
||||
isJWTEnabled() {
|
||||
return !!this.getSecurityConfig().enableJWT;
|
||||
},
|
||||
|
||||
getLoginUrl() {
|
||||
return this.getSecurityConfig().loginUrl;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a new "jenkinsConfig" object.
|
||||
* Useful for testing in a headless environment.
|
||||
* @param newConfig
|
||||
* @private
|
||||
*/
|
||||
_setJenkinsConfig(newConfig) {
|
||||
config.jenkinsConfig = newConfig;
|
||||
},
|
||||
};
|
|
@ -0,0 +1,308 @@
|
|||
import es6Promise from 'es6-promise'; es6Promise.polyfill();
|
||||
import jwt from './jwt';
|
||||
import isoFetch from 'isomorphic-fetch';
|
||||
import utils from './utils';
|
||||
import config from './config';
|
||||
import dedupe from './utils/dedupe-calls';
|
||||
import urlconfig from './urlconfig';
|
||||
import { prefetchdata } from './scopes';
|
||||
|
||||
const Promise = es6Promise.Promise;
|
||||
|
||||
import { capabilityAugmenter } from './capability/index';
|
||||
let refreshToken = null;
|
||||
export const FetchFunctions = {
|
||||
checkRefreshHeader(response) {
|
||||
const _refreshToken = response.headers.get('X-Blueocean-Refresher');
|
||||
// No token in response, lets just ignore.
|
||||
if (!_refreshToken) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// First time we have seen a refresh token, early exit.
|
||||
if (!refreshToken) {
|
||||
refreshToken = _refreshToken;
|
||||
return response;
|
||||
}
|
||||
|
||||
// We need to refresh the page now!
|
||||
if (refreshToken !== _refreshToken) {
|
||||
utils.refreshPage();
|
||||
throw new Error('refreshing apge');
|
||||
}
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* This method checks for for 2XX http codes. Throws error it it is not.
|
||||
* This should only be used if not using fetch or fetchJson.
|
||||
*/
|
||||
checkStatus(response) {
|
||||
if (response.status >= 300 || response.status < 200) {
|
||||
const error = new Error(response.statusText);
|
||||
error.response = response;
|
||||
throw error;
|
||||
}
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds same-origin option to the fetch.
|
||||
*/
|
||||
sameOriginFetchOption(options = {}) {
|
||||
const newOpts = utils.clone(options);
|
||||
newOpts.credentials = newOpts.credentials || 'same-origin';
|
||||
return newOpts;
|
||||
},
|
||||
|
||||
/**
|
||||
* Enhances the fetchOptions with the JWT bearer token. Will only be needed
|
||||
* if not using fetch or fetchJson.
|
||||
*/
|
||||
jwtFetchOption(token, options = {}) {
|
||||
const newOpts = utils.clone(options);
|
||||
newOpts.headers = newOpts.headers || {};
|
||||
newOpts.headers.Authorization = newOpts.headers.Authorization || `Bearer ${token}`;
|
||||
return newOpts;
|
||||
},
|
||||
|
||||
/**
|
||||
* REturns the json body from the response. It is only needed if
|
||||
* you are using FetchUtils.fetch
|
||||
*
|
||||
* Usage:
|
||||
* FetchUtils.fetch(..).then(FetchUtils.parseJSON)
|
||||
*/
|
||||
parseJSON(response) {
|
||||
return response.json()
|
||||
// FIXME: workaround for status=200 w/ empty response body that causes error in Chrome
|
||||
// server should probably return HTTP 204 instead
|
||||
.catch((error) => {
|
||||
if (error.message === 'Unexpected end of JSON input') {
|
||||
return {};
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
},
|
||||
|
||||
/* eslint-disable no-param-reassign */
|
||||
/**
|
||||
* Parses the response body for the error generated in checkStatus.
|
||||
*/
|
||||
parseErrorJson(error) {
|
||||
return error.response.json().then(
|
||||
body => {
|
||||
error.responseBody = body;
|
||||
throw error;
|
||||
},
|
||||
() => {
|
||||
error.responseBody = null;
|
||||
throw error;
|
||||
});
|
||||
},
|
||||
/* eslint-enable no-param-reassign */
|
||||
|
||||
/**
|
||||
* Error function helper to log errors to console.
|
||||
*
|
||||
* Usage;
|
||||
* fetchJson(..).catch(FetchUtils.consoleError)
|
||||
*/
|
||||
consoleError(error) {
|
||||
console.error(error); // eslint-disable-line no-console
|
||||
},
|
||||
|
||||
/**
|
||||
* Error function helper to call a callback on a rejected promise.
|
||||
* if callback is null, log to console). Use .catch() if you know it
|
||||
* will not be null though.
|
||||
*
|
||||
* Usage;
|
||||
* fetchJson(..).catch(FetchUtils.onError(error => //do something)
|
||||
*/
|
||||
onError(errorFunc) {
|
||||
return error => {
|
||||
if (errorFunc) {
|
||||
errorFunc(error);
|
||||
} else {
|
||||
FetchFunctions.consoleError(error);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Raw fetch that returns the json body.
|
||||
*
|
||||
* This method is semi-private, under normal conditions it should not be
|
||||
* used as it does not include the JWT bearer token
|
||||
*
|
||||
* @param {string} url - The URL to fetch from.
|
||||
* @param {Object} [options]
|
||||
* @param {function} [options.onSuccess] - Optional callback success function.
|
||||
* @param {function} [options.onError] - Optional error callback.
|
||||
* @param {Object} [options.fetchOptions] - Optional isomorphic-fetch options.
|
||||
* @returns JSON body
|
||||
*/
|
||||
rawFetchJSON(url, { onSuccess, onError, fetchOptions, disableDedupe } = {}) {
|
||||
const request = () => {
|
||||
let future = getPrefetchedDataFuture(url); // eslint-disable-line no-use-before-define
|
||||
if (!future) {
|
||||
future = isoFetch(url, FetchFunctions.sameOriginFetchOption(fetchOptions))
|
||||
.then(FetchFunctions.checkRefreshHeader)
|
||||
.then(FetchFunctions.checkStatus)
|
||||
.then(FetchFunctions.parseJSON, FetchFunctions.parseErrorJson);
|
||||
}
|
||||
if (onSuccess) {
|
||||
return future.then(onSuccess).catch(FetchFunctions.onError(onError));
|
||||
}
|
||||
|
||||
return future;
|
||||
};
|
||||
if (disableDedupe) {
|
||||
return request();
|
||||
}
|
||||
|
||||
return dedupe(url, request);
|
||||
},
|
||||
/**
|
||||
* Raw fetch.
|
||||
*
|
||||
* This method is semi-private, under normal conditions it should not be
|
||||
* used as it does not include the JWT bearer token
|
||||
*
|
||||
* @param {string} url - The URL to fetch from.
|
||||
* @param {Object} [options]
|
||||
* @param {function} [options.onSuccess] - Optional callback success function.
|
||||
* @param {function} [options.onError] - Optional error callback.
|
||||
* @param {Object} [options.fetchOptions] - Optional isomorphic-fetch options.
|
||||
* @returns fetch response
|
||||
*/
|
||||
rawFetch(url, { onSuccess, onError, fetchOptions, disableDedupe } = {}) {
|
||||
const request = () => {
|
||||
let future = getPrefetchedDataFuture(url); // eslint-disable-line no-use-before-define
|
||||
if (!future) {
|
||||
future = isoFetch(url, FetchFunctions.sameOriginFetchOption(fetchOptions))
|
||||
.then(FetchFunctions.checkRefreshHeader)
|
||||
.then(FetchFunctions.checkStatus);
|
||||
}
|
||||
if (onSuccess) {
|
||||
return future.then(onSuccess).catch(FetchFunctions.onError(onError));
|
||||
}
|
||||
return future;
|
||||
};
|
||||
|
||||
if (disableDedupe) {
|
||||
return request();
|
||||
}
|
||||
|
||||
return dedupe(url, request);
|
||||
},
|
||||
};
|
||||
|
||||
export const Fetch = {
|
||||
/**
|
||||
* Fetch JSON data.
|
||||
* <p>
|
||||
* Utility function that can be mocked for testing.
|
||||
*
|
||||
* @param {string} url - The URL to fetch from.
|
||||
* @param {Object} [options]
|
||||
* @param {function} [options.onSuccess] - Optional callback success function.
|
||||
* @param {function} [options.onError] - Optional error callback.
|
||||
* @param {Object} [options.fetchOptions] - Optional isomorphic-fetch options.
|
||||
* @returns JSON body.
|
||||
*/
|
||||
fetchJSON(url, { onSuccess, onError, fetchOptions, disableCapabilites } = {}) {
|
||||
let fixedUrl = url;
|
||||
if (urlconfig.getJenkinsRootURL() !== '' && !url.startsWith(urlconfig.getJenkinsRootURL())) {
|
||||
fixedUrl = `${urlconfig.getJenkinsRootURL()}${url}`;
|
||||
}
|
||||
let future;
|
||||
if (!config.isJWTEnabled()) {
|
||||
future = FetchFunctions.rawFetchJSON(fixedUrl, { onSuccess, onError, fetchOptions });
|
||||
} else {
|
||||
future = jwt.getToken()
|
||||
.then(token => FetchFunctions.rawFetchJSON(fixedUrl, {
|
||||
onSuccess,
|
||||
onError,
|
||||
fetchOptions: FetchFunctions.jwtFetchOption(token, fetchOptions),
|
||||
}));
|
||||
}
|
||||
|
||||
if (!disableCapabilites) {
|
||||
return future.then(data => capabilityAugmenter.augmentCapabilities(utils.clone(data)));
|
||||
}
|
||||
|
||||
return future;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch data.
|
||||
* <p>
|
||||
* Utility function that can be mocked for testing.
|
||||
*
|
||||
* @param {string} url - The URL to fetch from.
|
||||
* @param {Object} [options]
|
||||
* @param {function} [options.onSuccess] - Optional callback success function.
|
||||
* @param {function} [options.onError] - Optional error callback.
|
||||
* @param {Object} [options.fetchOptions] - Optional isomorphic-fetch options.
|
||||
* @returns fetch body.
|
||||
*/
|
||||
fetch(url, { onSuccess, onError, fetchOptions } = {}) {
|
||||
let fixedUrl = url;
|
||||
|
||||
if (urlconfig.getJenkinsRootURL() !== '' && !url.startsWith(urlconfig.getJenkinsRootURL())) {
|
||||
fixedUrl = `${urlconfig.getJenkinsRootURL()}${url}`;
|
||||
}
|
||||
if (!config.isJWTEnabled()) {
|
||||
return FetchFunctions.rawFetch(fixedUrl, { onSuccess, onError, fetchOptions });
|
||||
}
|
||||
|
||||
return jwt.getToken()
|
||||
.then(token => FetchFunctions.rawFetch(fixedUrl, {
|
||||
onSuccess,
|
||||
onError,
|
||||
fetchOptions: FetchFunctions.jwtFetchOption(token, fetchOptions),
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
function trimRestUrl(url) {
|
||||
const REST_PREFIX = 'blue/rest/organizations';
|
||||
const prefixOffset = url.indexOf(REST_PREFIX);
|
||||
|
||||
if (prefixOffset !== -1) {
|
||||
return url.substring(prefixOffset);
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
function getPrefetchedDataFuture(url) {
|
||||
const trimmedUrl = trimRestUrl(url);
|
||||
|
||||
for (const prop in prefetchdata) {
|
||||
if (prefetchdata.hasOwnProperty(prop)) {
|
||||
const preFetchEntry = prefetchdata[prop];
|
||||
if (preFetchEntry.restUrl && preFetchEntry.data) {
|
||||
// If the trimmed/normalized rest URL matches the url arg supplied
|
||||
// to the function, construct a pre-resolved future object containing
|
||||
// the prefetched data as the value.
|
||||
if (trimRestUrl(preFetchEntry.restUrl) === trimmedUrl) {
|
||||
try {
|
||||
return Promise.resolve(JSON.parse(preFetchEntry.data));
|
||||
} finally {
|
||||
// Delete the preFetchEntry i.e. we only use these entries once. So, this
|
||||
// works only for the first request for the data at that URL. Subsequent
|
||||
// calls on that REST endpoint will result in a proper fetch. A local
|
||||
// store needs to be used (redux/mobx etc) if you want to avoid multiple calls
|
||||
// for the same data. This is not a caching layer/mechanism !!!
|
||||
delete prefetchdata[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
import i18next from 'i18next';
|
||||
import LngDetector from 'i18next-browser-languagedetector';
|
||||
import XHR from 'i18next-xhr-backend';
|
||||
import { store } from '@jenkins-cd/js-extensions';
|
||||
|
||||
import urlConfig from '../urlconfig';
|
||||
|
||||
/**
|
||||
* Init language detector, we are going to use first queryString and then the navigator prefered language
|
||||
*/
|
||||
export const defaultLngDetector = new LngDetector(null, {
|
||||
// order and from where user language should be detected
|
||||
order: ['querystring', 'navigator'],
|
||||
// keys or params to lookup language from
|
||||
lookupQuerystring: 'language',
|
||||
});
|
||||
const prefix = urlConfig.getJenkinsRootURL() || '';
|
||||
const FALLBACK_LANG = '';
|
||||
|
||||
function newPluginXHR(pluginName) {
|
||||
let pluginVersion = store.getPluginVersion(pluginName);
|
||||
|
||||
if (!pluginVersion) {
|
||||
throw new Error(`Unable to create an i18n instance for plugin "${pluginName}". This plugin is not currently installed, or is disabled.`);
|
||||
}
|
||||
|
||||
pluginVersion = encodeURIComponent(pluginVersion);
|
||||
|
||||
const loadPath = `${prefix}/blue/rest/i18n/${pluginName}/${pluginVersion}/{ns}/{lng}`;
|
||||
return new XHR(null, {
|
||||
loadPath,
|
||||
allowMultiLoading: false,
|
||||
parse: (data) => {
|
||||
// we need to parse the response and then extract the data since the rest is garbage for us
|
||||
const response = JSON.parse(data);
|
||||
return response.data;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a instance of i18next and init it
|
||||
* in case we are in test mode and run unit test, we deliver a i18next instance that are not using any backend nor language detection
|
||||
* @param backend {object} - the backend XHR invoker we want to use
|
||||
* @param lngDetector {object} - the component that detects which language we want to display
|
||||
* @param options {object} - general options for i18next
|
||||
* @see defaultOptions
|
||||
*/
|
||||
const i18nextInstance = (backend, lngDetector = defaultLngDetector, options) => {
|
||||
if (!backend) {
|
||||
throw new Error('Invalid call to create a new i18next instance. No backend XHR invoker supplied.');
|
||||
}
|
||||
if (!options) {
|
||||
throw new Error('Invalid call to create a new i18next instance. No i18next options supplied.');
|
||||
}
|
||||
return i18next.createInstance()
|
||||
.use(backend)
|
||||
.use(lngDetector)
|
||||
.init(options);
|
||||
};
|
||||
|
||||
const translatorCache = {};
|
||||
let useMockFallback = false;
|
||||
|
||||
const assertPluginNameDefined = (pluginName) => {
|
||||
if (!pluginName) {
|
||||
throw new Error('"pluginName" arg cannot be null/blank');
|
||||
}
|
||||
};
|
||||
|
||||
const toDefaultNamespace = (pluginName) => {
|
||||
assertPluginNameDefined(pluginName);
|
||||
// Replace all hyphen chars with a dot.
|
||||
return `jenkins.plugins.${pluginName.replace(/-/g, '.')}.Messages`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an i18next instance for accessing i18n resource bundles
|
||||
* in the named plugin namespace.
|
||||
* @param pluginName The name of the plugin.
|
||||
* @param namespace The resource bundle namespace. Optional, defaulting to
|
||||
* the plugin's default resource bundle e.g. "jenkins.plugins.blueocean.web.Messages"
|
||||
* for the "blueocean-web" plugin and "jenkins.plugins.blueocean.dashboard.Messages"
|
||||
* for the "blueocean-dashboard" plugin.
|
||||
* @return An i18n instance.
|
||||
*/
|
||||
const pluginI18next = (pluginName, namespace = toDefaultNamespace(pluginName)) => {
|
||||
assertPluginNameDefined(pluginName);
|
||||
|
||||
const initOptions = {
|
||||
ns: [namespace],
|
||||
defaultNS: namespace,
|
||||
keySeparator: false, // we do not have any nested keys in properties files
|
||||
debug: false,
|
||||
fallbackLng: FALLBACK_LANG,
|
||||
load: 'currentOnly',
|
||||
interpolation: {
|
||||
prefix: '{',
|
||||
suffix: '}',
|
||||
escapeValue: false, // not needed for react!!
|
||||
},
|
||||
};
|
||||
|
||||
return i18nextInstance(newPluginXHR(pluginName), defaultLngDetector, initOptions);
|
||||
};
|
||||
|
||||
function buildCacheKey(pluginName, namespace = toDefaultNamespace(pluginName)) {
|
||||
return `${pluginName}:${namespace}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an i18n Translator instance for accessing i18n resource bundles
|
||||
* in the named plugin namespace.
|
||||
* @param pluginName The name of the plugin.
|
||||
* @param namespace The resource bundle namespace. Optional, defaulting to
|
||||
* the plugin's default resource bundle e.g. "jenkins.plugins.blueocean.web.Messages"
|
||||
* for the "blueocean-web" plugin and "jenkins.plugins.blueocean.dashboard.Messages"
|
||||
* for the "blueocean-dashboard" plugin.
|
||||
* @return An i18n Translator instance.
|
||||
*/
|
||||
export default function i18nTranslator(pluginName, namespace) {
|
||||
assertPluginNameDefined(pluginName);
|
||||
|
||||
const translatorCacheKey = buildCacheKey(pluginName, namespace);
|
||||
let translator = translatorCache[translatorCacheKey];
|
||||
|
||||
if (translator) {
|
||||
return translator;
|
||||
}
|
||||
|
||||
if (useMockFallback) {
|
||||
return function mockTranslate(key) {
|
||||
return key;
|
||||
};
|
||||
}
|
||||
|
||||
const I18n = pluginI18next(pluginName, namespace);
|
||||
|
||||
// Create and cache the translator instance.
|
||||
let detectedLang;
|
||||
try {
|
||||
detectedLang = defaultLngDetector.detect();
|
||||
} catch (e) {
|
||||
detectedLang = FALLBACK_LANG;
|
||||
}
|
||||
translator = I18n.getFixedT(detectedLang, namespace);
|
||||
translatorCache[translatorCacheKey] = translator;
|
||||
|
||||
return translator;
|
||||
}
|
||||
|
||||
export function enableMocks() {
|
||||
useMockFallback = true;
|
||||
}
|
||||
|
||||
export function disableMocks() {
|
||||
useMockFallback = false;
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
|
||||
/**
|
||||
* Created by cmeyers on 8/18/16.
|
||||
*/
|
||||
|
||||
import { Fetch } from './fetch';
|
||||
import * as sse from '@jenkins-cd/sse-gateway';
|
||||
import { RunApi } from './rest/RunApi';
|
||||
|
||||
import { SseBus } from './sse/SseBus';
|
||||
import { ToastService } from './ToastService';
|
||||
|
||||
// export i18n provider
|
||||
export i18nTranslator, { defaultLngDetector } from './i18n/i18n';
|
||||
|
||||
export { Fetch, FetchFunctions } from './fetch';
|
||||
export UrlBuilder from './UrlBuilder';
|
||||
export UrlConfig from './urlconfig';
|
||||
export JWT from './jwt';
|
||||
export TestUtils from './testutils';
|
||||
export ToastUtils from './ToastUtils';
|
||||
export Utils from './utils';
|
||||
export { User } from './User';
|
||||
export AppConfig from './config';
|
||||
export Security from './security';
|
||||
export Paths from './paths/index';
|
||||
|
||||
import { Pager, PagerService, PipelineService, SSEService, ActivityService, DefaultSSEHandler, LocationService } from './services/index';
|
||||
export { Pager, PagerService, PipelineService, SSEService, ActivityService };
|
||||
|
||||
|
||||
export Fullscreen from './Fullscreen';
|
||||
export NotFound from './NotFound';
|
||||
|
||||
export { ReplayButton } from './components/ReplayButton';
|
||||
export { RunButton } from './components/RunButton';
|
||||
|
||||
// Create and export the SSE connection that will be shared by other
|
||||
// Blue Ocean components via this package.
|
||||
export const sseConnection = sse.connect('jenkins-blueocean-core-js');
|
||||
|
||||
// export services as a singleton so all plugins will use the same instance
|
||||
|
||||
// capabilities
|
||||
export { capable, capabilityStore, capabilityAugmenter } from './capability/index';
|
||||
|
||||
// limit to single instance so that duplicate REST calls aren't made as events come in
|
||||
const sseBus = new SseBus(sseConnection, Fetch.fetchJSON);
|
||||
export { sseBus as SseBus };
|
||||
|
||||
// required so new toasts are routed to the instance used in blueocean-web
|
||||
const toastService = new ToastService();
|
||||
export { toastService as ToastService };
|
||||
|
||||
const runApi = new RunApi();
|
||||
export { runApi as RunApi };
|
||||
|
||||
export const pagerService = new PagerService();
|
||||
export const sseService = new SSEService(sseConnection);
|
||||
export const activityService = new ActivityService(pagerService);
|
||||
export const pipelineService = new PipelineService(pagerService, activityService);
|
||||
export const locationService = new LocationService();
|
||||
|
||||
const defaultSSEhandler = new DefaultSSEHandler(pipelineService, activityService, pagerService);
|
||||
sseService.registerHandler(defaultSSEhandler.handleEvents);
|
|
@ -0,0 +1,101 @@
|
|||
import es6Promise from 'es6-promise'; es6Promise.polyfill();
|
||||
import fetch from 'isomorphic-fetch';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { BlueUrl as UrlUtils } from './urlconfig';
|
||||
import { FetchFunctions } from './fetch';
|
||||
import { jwk2pem } from 'pem-jwk';
|
||||
let storedToken = null;
|
||||
let publicKeyStore = null;
|
||||
let tokenFetchPromise = null;
|
||||
|
||||
const CLOCK_SKEW_SECONDS = 60;
|
||||
export default {
|
||||
/**
|
||||
* Fetches the JWT token. This token is cached for a default of 25mins.
|
||||
* If it is within 5mins or expiry it will fetch a new one.
|
||||
*/
|
||||
fetchJWT() {
|
||||
if (storedToken && storedToken.exp) {
|
||||
const diff = storedToken.exp - Math.trunc(new Date().getTime() / 1000);
|
||||
|
||||
// refetch token if we are within 60s of it exp
|
||||
if (diff < CLOCK_SKEW_SECONDS) {
|
||||
tokenFetchPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!tokenFetchPromise) {
|
||||
tokenFetchPromise = fetch(`${UrlUtils.getJenkinsRootURL()}/jwt-auth/token`, { credentials: 'same-origin' })
|
||||
.then(this.checkStatus)
|
||||
.then(response => {
|
||||
const token = response.headers.get('X-BLUEOCEAN-JWT');
|
||||
if (token) {
|
||||
return token;
|
||||
}
|
||||
|
||||
throw new Error('Could not fetch jwt_token');
|
||||
});
|
||||
}
|
||||
|
||||
return tokenFetchPromise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies the token using the public key.
|
||||
*/
|
||||
verifyToken(token, certObject) {
|
||||
return new Promise((resolve, reject) =>
|
||||
jwt.verify(token, jwk2pem(certObject), { algorithms: [certObject.alg], clockTolerance: CLOCK_SKEW_SECONDS }, (err, payload) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(payload);
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches the public key that is used to verify tokens.
|
||||
*/
|
||||
fetchJWTPublicKey(token) {
|
||||
const decoded = jwt.decode(token, { complete: true });
|
||||
const url = `${UrlUtils.getJenkinsRootURL()}/jwt-auth/jwks/${decoded.header.kid}/`;
|
||||
if (!publicKeyStore) {
|
||||
publicKeyStore = fetch(url, { credentials: 'same-origin' })
|
||||
.then(FetchFunctions.checkStatus)
|
||||
.then(FetchFunctions.parseJSON)
|
||||
.then(cert => this.verifyToken(token, cert)
|
||||
.then(payload => ({
|
||||
token,
|
||||
payload,
|
||||
})));
|
||||
}
|
||||
|
||||
return publicKeyStore;
|
||||
},
|
||||
|
||||
/**
|
||||
* Puts the token into global storage for later use.
|
||||
*/
|
||||
storeToken(data) {
|
||||
storedToken = data.payload;
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Use this function if you want the payload from the token.
|
||||
*/
|
||||
getTokenWithPayload() {
|
||||
return this.fetchJWT()
|
||||
.then(FetchFunctions.checkStatus)
|
||||
.then(token => this.fetchJWTPublicKey(token))
|
||||
.then(data => this.storeToken(data));
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the token from te server and verifies it.
|
||||
*/
|
||||
getToken() {
|
||||
return this.getTokenWithPayload().then(token => token.token);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
|
||||
import { observable, action, asMap } from 'mobx';
|
||||
export class DataBunker {
|
||||
@observable _data = asMap();
|
||||
|
||||
constructor(keyFn, mapperFn) {
|
||||
this._keyFn = keyFn;
|
||||
this._mapperFn = mapperFn;
|
||||
if (!mapperFn) {
|
||||
// identity function.
|
||||
this._mapperFn = x => x;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
setItem(item) {
|
||||
const keyItem = this._keyFn(item);
|
||||
const mappedItem = this._mapperFn(item);
|
||||
this._data.set(keyItem, mappedItem);
|
||||
return mappedItem;
|
||||
}
|
||||
|
||||
setItems(items) {
|
||||
return items.map(item => this.setItem(item));
|
||||
}
|
||||
|
||||
getItem(key) {
|
||||
return this._data.get(key);
|
||||
}
|
||||
|
||||
@action
|
||||
removeItem(key) {
|
||||
console.log('before', this._data);
|
||||
console.log('successful', this._data.delete(key));
|
||||
console.log('after', this._data);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// @flow
|
||||
export type LinkObject = {
|
||||
[ id: string ] : { href: string}
|
||||
}
|
||||
|
||||
export type ActivityModel = {
|
||||
organization: string,
|
||||
pipeline: string,
|
||||
_links: LinkObject,
|
||||
changeSet: Object,
|
||||
durationInMillis: number,
|
||||
estimatedDurationInMillis: number,
|
||||
id: string,
|
||||
result: string,
|
||||
state: string,
|
||||
startTime: string,
|
||||
endTime: string;
|
||||
commitId: string
|
||||
}
|
||||
|
||||
export type PipelineModel = {
|
||||
name: string,
|
||||
fullName: string,
|
||||
organization: string,
|
||||
numberOfSuccessfulBranches: number,
|
||||
numberOfFailingBranches: number,
|
||||
numberOfSuccessfulPullRequests: number,
|
||||
numberOfFailingPullRequests: number,
|
||||
displayName: string,
|
||||
weatherScore: string,
|
||||
_links: LinkObject,
|
||||
_class: string,
|
||||
latestRun: ActivityModel
|
||||
}
|
||||
|
||||
export type BranchModel = PipelineModel & {
|
||||
pullRequest: Object
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import rest from './rest';
|
||||
export default {
|
||||
rest,
|
||||
};
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* This object defines rest paths
|
||||
*/
|
||||
export default {
|
||||
_convertSlashes(pipeline) {
|
||||
return pipeline.replace(/\//g, '/pipelines/');
|
||||
},
|
||||
|
||||
apiRoot() {
|
||||
return '/blue/rest';
|
||||
},
|
||||
|
||||
organizationPipelines(organizationName) {
|
||||
return `${this.apiRoot()}/search/?q=type:pipeline;organization:${encodeURIComponent(organizationName)};excludedFromFlattening:jenkins.branch.MultiBranchProject,hudson.matrix.MatrixProject&filter=no-folders`;
|
||||
},
|
||||
|
||||
allPipelines() {
|
||||
return `${this.apiRoot()}/search/?q=type:pipeline;excludedFromFlattening:jenkins.branch.MultiBranchProject,hudson.matrix.MatrixProject&filter=no-folders`;
|
||||
},
|
||||
|
||||
activities(organization, pipeline) {
|
||||
return `${this.apiRoot()}/organizations/${encodeURIComponent(organization)}/pipelines/${pipeline}/activities/`;
|
||||
},
|
||||
|
||||
run({ organization, pipeline, branch, runId }) {
|
||||
if (branch) {
|
||||
return `${this.pipeline(organization, pipeline)}branches/${encodeURIComponent(encodeURIComponent(branch))}/runs/${runId}/`;
|
||||
}
|
||||
|
||||
return `${this.pipeline(organization, pipeline)}runs/${runId}/`;
|
||||
},
|
||||
|
||||
pipeline(organization, pipeline) {
|
||||
return `${this.apiRoot()}/organizations/${encodeURIComponent(organization)}/pipelines/${this._convertSlashes(pipeline)}/`;
|
||||
},
|
||||
branches(organization, pipeline) {
|
||||
return `${this.apiRoot()}/organizations/${encodeURIComponent(organization)}/pipelines/${pipeline}/branches/?filter=origin`;
|
||||
},
|
||||
|
||||
pullRequests(organization, pipeline) {
|
||||
return `${this.apiRoot()}/organizations/${encodeURIComponent(organization)}/pipelines/${pipeline}/branches/?filter=pull-requests`;
|
||||
},
|
||||
|
||||
queuedItem(organization, pipeline, queueId) {
|
||||
return `${this.pipeline(organization, pipeline)}queue/${queueId}/`;
|
||||
},
|
||||
};
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* Created by cmeyers on 8/29/16.
|
||||
*/
|
||||
import { Fetch } from '../fetch';
|
||||
import config from '../urlconfig';
|
||||
import utils from '../utils';
|
||||
|
||||
export class RunApi {
|
||||
|
||||
startRun(item) {
|
||||
const path = config.getJenkinsRootURL();
|
||||
const runUrl = utils.cleanSlashes(`${path}/${item._links.self.href}/runs/`);
|
||||
|
||||
const fetchOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
return Fetch.fetchJSON(runUrl, { fetchOptions });
|
||||
}
|
||||
|
||||
stopRun(run) {
|
||||
const path = config.getJenkinsRootURL();
|
||||
const runUrl = run._links.self.href;
|
||||
const stopUrl = utils.cleanSlashes(`${path}/${runUrl}/stop/?blocking=true&timeOutInSecs=10`);
|
||||
|
||||
const fetchOptions = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
return Fetch.fetch(stopUrl, { fetchOptions });
|
||||
}
|
||||
|
||||
removeFromQueue(queueItem) {
|
||||
const path = config.getJenkinsRootURL();
|
||||
let queueItemUrl;
|
||||
|
||||
// a queue item is a "pseudo run" with the queue href attached via _item
|
||||
if (queueItem._item && queueItem._item._links) {
|
||||
queueItemUrl = queueItem._item._links.self.href;
|
||||
} else {
|
||||
console.warn('could not extract data to remove item from queue; aborting');
|
||||
return null;
|
||||
}
|
||||
|
||||
const removeQueueUrl = utils.cleanSlashes(`${path}/${queueItemUrl}`);
|
||||
|
||||
const fetchOptions = {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
return Fetch.fetch(removeQueueUrl, { fetchOptions });
|
||||
}
|
||||
|
||||
replayRun(run) {
|
||||
const path = config.getJenkinsRootURL();
|
||||
const runUrl = run._links.self.href;
|
||||
const replayPipelineUrl = utils.cleanSlashes(`${path}/${runUrl}/replay/`);
|
||||
|
||||
const fetchOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
return Fetch.fetchJSON(replayPipelineUrl, { fetchOptions });
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2016, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export const root = (typeof self === 'object' && self.self === self && self) ||
|
||||
(typeof global === 'object' && global.global === global && global) || this;
|
||||
|
||||
//
|
||||
// See blueocean-config/src/main/java/io/jenkins/blueocean/config/BlueOceanConfig.java
|
||||
// and blueocean-config/src/main/resources/io/jenkins/blueocean/config/BlueOceanConfig/header.jelly
|
||||
//
|
||||
export const blueocean = (root.$blueocean || {});
|
||||
|
||||
export const prefetchdata = (blueocean.prefetchdata || {});
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* Created by cmeyers on 9/16/16.
|
||||
*/
|
||||
|
||||
import config from './config';
|
||||
import { User } from './User';
|
||||
|
||||
/**
|
||||
* Returns a key of permissions functions that each return boolean to indicate authorization.
|
||||
* Usage:
|
||||
* permit(pipeline).create();
|
||||
*
|
||||
* @param subject
|
||||
* @returns {{read: (function()), create: (function()), start: (function()), stop: (function())}}
|
||||
*/
|
||||
const permit = (subject) => {
|
||||
const checkPermissions = (permissionName) => (
|
||||
subject && subject.permissions && !!subject.permissions[permissionName]
|
||||
);
|
||||
|
||||
return {
|
||||
read: () => checkPermissions('read'),
|
||||
create: () => checkPermissions('create'),
|
||||
start: () => checkPermissions('start'),
|
||||
stop: () => checkPermissions('stop'),
|
||||
};
|
||||
};
|
||||
|
||||
function isSecurityEnabled() {
|
||||
return !!config.getSecurityConfig().enabled;
|
||||
}
|
||||
|
||||
function isAnonymousUser() {
|
||||
return User.current().isAnonymous();
|
||||
}
|
||||
|
||||
export default {
|
||||
permit,
|
||||
isSecurityEnabled,
|
||||
isAnonymousUser,
|
||||
};
|
|
@ -0,0 +1,178 @@
|
|||
import { Pager } from './Pager';
|
||||
import RestPaths from '../paths/rest';
|
||||
import { Fetch } from '../fetch';
|
||||
import { BunkerService } from './BunkerService';
|
||||
import utils from '../utils';
|
||||
import mobxUtils from 'mobx-utils';
|
||||
|
||||
/*
|
||||
* This class provides activity related services.
|
||||
*
|
||||
* @export
|
||||
* @class ActivityService
|
||||
* @extends {BunkerService}
|
||||
*/
|
||||
export class ActivityService extends BunkerService {
|
||||
/**
|
||||
* Generates a pager key for [@link PagerService] to store the [@link Pager] under.
|
||||
*
|
||||
* @param {string} organization Jenkins organization that this pager belongs to.
|
||||
* @param {string} pipeline Pipeline that this pager belongs to.
|
||||
* @returns {string} key for [@link PagerService]
|
||||
*/
|
||||
pagerKey(organization, pipeline) {
|
||||
return `Activities/${organization}-${pipeline}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the activity pager
|
||||
*
|
||||
* @param {string} organization Jenkins organization that this pager belongs to.
|
||||
* @param {string} pipeline Pipeline that this pager belongs to.
|
||||
* @returns {Pager} Pager for this pipelne.
|
||||
*/
|
||||
activityPager(organization, pipeline) {
|
||||
return this.pagerService.getPager({
|
||||
key: this.pagerKey(organization, pipeline),
|
||||
/**
|
||||
* Lazily generate the pager incase its needed.
|
||||
*/
|
||||
lazyPager: () => new Pager(RestPaths.activities(organization, pipeline), 25, this),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps queued data into a psudeorun
|
||||
*
|
||||
* @see _mapQueueToPsuedoRun
|
||||
*
|
||||
* @param {Object} data Raw data from extenal source.
|
||||
* @returns A run or psudeorun.
|
||||
*/
|
||||
bunkerMapper(data) {
|
||||
return this._mapQueueToPsuedoRun(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an activity from the store.
|
||||
*
|
||||
* @param {string} href Self href for activity.
|
||||
* @returns {object} Mobx computed value
|
||||
*/
|
||||
getActivity(href) {
|
||||
return this.getItem(href);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an activity from rest api.
|
||||
*
|
||||
* Note: This only works for activities that are not in the queue.
|
||||
*
|
||||
* @param {string} href self href of activity.
|
||||
* @param {boolean} useCache Use the cache to lookup data or always fetch a new one.
|
||||
* @param {boolean} overrideQueuedState Hack to make SSE work. Not use unless you know what you are doing!!!
|
||||
* @returns {Promise} Promise of fetched data.
|
||||
*/
|
||||
fetchActivity(href, { useCache, overrideQueuedState } = {}) {
|
||||
if (useCache && this.hasItem(href)) {
|
||||
return Promise.resolve(this.getItem(href));
|
||||
}
|
||||
|
||||
|
||||
return Fetch.fetchJSON(href)
|
||||
.then(data => {
|
||||
// Should really have dedupe on methods like these, but for now
|
||||
// just clone data so that we dont modify other instances.
|
||||
const run = utils.clone(data);
|
||||
|
||||
// Ugly hack to make SSE work.
|
||||
if (overrideQueuedState) {
|
||||
run.state = 'RUNNING';
|
||||
run.result = 'UNKNOWN';
|
||||
}
|
||||
return this.setItem(run);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('There has been an error while trying to get the data.', err); // FIXME: Ivan what is the way to return an "error" opbject so underlying component are aware of the problem and can react
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches artifacts for a given run.
|
||||
*
|
||||
* @param {string} runHref The href of the run to fetcfh artifacts for.
|
||||
* @returns {Object} Object containing zipFile link and list of artifacts.
|
||||
*/
|
||||
fetchArtifacts(runHref) {
|
||||
return mobxUtils.fromPromise(Fetch.fetchJSON(`${runHref}artifacts/?start=0&limit=101`));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function maps a queue item into a run instancce.
|
||||
*
|
||||
* We do this because the api returns us queued items as well
|
||||
* as runs and its easier to deal with them if they are modeled
|
||||
* as the same thing. If the raw data is needed if can be fetched
|
||||
* from _item.
|
||||
*
|
||||
* @param {object} run Raw data from api.
|
||||
* @returns psudeorun
|
||||
*/
|
||||
_mapQueueToPsuedoRun(run) {
|
||||
if (run._class === 'io.jenkins.blueocean.service.embedded.rest.QueueItemImpl') {
|
||||
return {
|
||||
id: String(run.expectedBuildNumber),
|
||||
state: 'QUEUED',
|
||||
pipeline: run.pipeline,
|
||||
type: 'QueuedItem',
|
||||
result: 'UNKNOWN',
|
||||
job_run_queueId: run.id,
|
||||
enQueueTime: run.queuedTime,
|
||||
organization: run.organization,
|
||||
changeSet: [],
|
||||
_links: {
|
||||
self: {
|
||||
href: `${run._links.parent.href}runs/${run.expectedBuildNumber}/`,
|
||||
},
|
||||
parent: {
|
||||
href: run._links.parent.href,
|
||||
},
|
||||
},
|
||||
_item: run,
|
||||
};
|
||||
}
|
||||
return run;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculate an expected build number for a queued item.
|
||||
*
|
||||
* TODO: Enhance SSE so that this is done server side.
|
||||
*
|
||||
* @param {any} event SSE event.
|
||||
* @returns {number} Expected build number
|
||||
*/
|
||||
getExpectedBuildNumber(event) {
|
||||
const runs = this._data.values();
|
||||
const eventJobUrl = event.blueocean_job_rest_url;
|
||||
let nextId = 0;
|
||||
for (let i = 0; i < runs.length; i++) {
|
||||
const run = runs[i];
|
||||
if (eventJobUrl !== run._links.parent.href) {
|
||||
continue;
|
||||
}
|
||||
if (run.job_run_queueId === event.job_run_queueId) {
|
||||
// We already have a "dummy" record for this queued job
|
||||
// run. No need to create another i.e. ignore this event.
|
||||
return run.id;
|
||||
}
|
||||
if (parseInt(run.id, 10) > nextId) { // figure out the next id, expectedBuildNumber
|
||||
nextId = parseInt(run.id, 10);
|
||||
}
|
||||
}
|
||||
|
||||
return nextId + 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
import { observable, computed, action, asMap } from 'mobx';
|
||||
|
||||
/**
|
||||
* Abstract class used by services that need to store data in a key/value store.
|
||||
*
|
||||
* It is designed to store json objects from a rest api.
|
||||
*
|
||||
* @export
|
||||
* @class BunkerService
|
||||
*/
|
||||
export class BunkerService {
|
||||
/**
|
||||
* Private mobx map for storing data.
|
||||
*/
|
||||
@observable _data = asMap();
|
||||
|
||||
/**
|
||||
* Creates an instance of BunkerService.
|
||||
*
|
||||
* @param {PagerService} pagerService
|
||||
*/
|
||||
constructor(pagerService) {
|
||||
this.pagerService = pagerService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the key to store the daata under out of the object
|
||||
*
|
||||
* Default impl uses the self href link in BlueOcuean objects.
|
||||
*
|
||||
* @param {object} data Data to be stored once it has been passed through
|
||||
* [@link bunkerMapper]
|
||||
* @returns {any} The key for the store.
|
||||
*/
|
||||
bunkerKey(data) {
|
||||
return data._links.self.href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the data from the source into what needs to be stored.
|
||||
*
|
||||
* Default impl is identity.
|
||||
*
|
||||
* @param {object} data Raw data from external source.
|
||||
* @returns {object} Modified data object.
|
||||
*/
|
||||
bunkerMapper(data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that will make all pagers using this bunker refetch the data
|
||||
* they are displaying. Useful if sorting changes (for example a new item is added).
|
||||
*/
|
||||
refreshPagers() {
|
||||
this.pagerService.refresh(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an item in the store.
|
||||
*
|
||||
* It uses the [@link bunkerKey] and [@link bunkerMapper] to generate the key/value
|
||||
* to be stored.
|
||||
*
|
||||
* @param {Object} item Raw data from extenal source.
|
||||
* @returns {Object} item mapped by [@link bunkerMapper]. It is also a mobx computed value.
|
||||
*/
|
||||
@action
|
||||
setItem(item) {
|
||||
const mappedItem = observable(this.bunkerMapper(item));
|
||||
const keyItem = this.bunkerKey(mappedItem);
|
||||
this._data.set(keyItem, mappedItem);
|
||||
return this.getItem(keyItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an array on item in the store. Calls [@link setItem] for even item in array.
|
||||
*
|
||||
* @param {Object[]} items Array of items to set.
|
||||
* @returns {Object[]} Array of mobx computed values from store.
|
||||
*/
|
||||
setItems(items) {
|
||||
return items.map(item => this.setItem(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets item from store.
|
||||
*
|
||||
* @param {any} key Key of item in store.
|
||||
* @returns {Object} Mobx computed value of value in store.
|
||||
*/
|
||||
getItem(key) {
|
||||
return computed(() => this._data.get(key)).get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes item from store.
|
||||
*
|
||||
* @param {any} key Key of item in store.
|
||||
*/
|
||||
@action
|
||||
removeItem(key) {
|
||||
this._data.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests to see if item exists in store.
|
||||
*
|
||||
* @param {any} key Key of item in store.
|
||||
* @returns {boolean} true if item exists in store.
|
||||
*/
|
||||
hasItem(key) {
|
||||
return this._data.has(key);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
|
||||
export class DefaultSSEHandler {
|
||||
constructor(pipelineService, activityService, pagerService) {
|
||||
this.pipelineService = pipelineService;
|
||||
this.activityService = activityService;
|
||||
this.pagerService = pagerService;
|
||||
}
|
||||
|
||||
handleEvents = (event) => {
|
||||
switch (event.jenkins_event) {
|
||||
case 'job_run_paused':
|
||||
case 'job_run_unpaused':
|
||||
this.updateJob(event);
|
||||
break;
|
||||
case 'job_crud_created':
|
||||
// Refetch pagers here. This will pull in the newly created pipeline into the bunker.
|
||||
this.pipelineService.refreshPagers();
|
||||
break;
|
||||
case 'job_crud_deleted':
|
||||
// Remove directly from bunker. No need to refresh bunkers as it will just show one less item.
|
||||
this.pipelineService.removeItem(event.blueocean_job_rest_url);
|
||||
break;
|
||||
case 'job_crud_renamed':
|
||||
// TODO: Implement this.
|
||||
// Seems to be that SSE fires an updated event for the old job,
|
||||
// then a rename for the new one. This is somewhat confusing for us.
|
||||
break;
|
||||
case 'job_run_queue_buildable':
|
||||
case 'job_run_queue_enter':
|
||||
this.queueEnter(event);
|
||||
break;
|
||||
case 'job_run_queue_left':
|
||||
// this.props.processJobLeftQueueEvent(eventCopy);
|
||||
break;
|
||||
case 'job_run_queue_blocked': {
|
||||
break;
|
||||
}
|
||||
case 'job_run_started': {
|
||||
this.updateJob(event, true);
|
||||
break;
|
||||
}
|
||||
case 'job_run_ended': {
|
||||
this.updateJob(event);
|
||||
break;
|
||||
}
|
||||
default :
|
||||
// Else ignore the event.
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
updateJob(event, overrideQueuedState) {
|
||||
// const queueId = event.job_run_queueId;
|
||||
// const queueSelf = `${event.blueocean_job_rest_url}queue/${queueId}/`;
|
||||
const runSelf = `${event.blueocean_job_rest_url}runs/${event.jenkins_object_id}/`;
|
||||
|
||||
const key = this.activityService.pagerKey(event.jenkins_org, event.blueocean_job_pipeline_name);
|
||||
const pager = this.pagerService.getPager({ key });
|
||||
this.activityService.fetchActivity(runSelf, { overrideQueuedState }).then(d => {
|
||||
if (pager && !pager.has(runSelf)) {
|
||||
pager.insert(runSelf);
|
||||
}
|
||||
this.pipelineService.updateLatestRun(d);
|
||||
});
|
||||
}
|
||||
queueCancel(event) {
|
||||
if (event.job_run_status === 'CANCELLED') {
|
||||
const queueId = event.job_run_queueId;
|
||||
const self = `${event.blueocean_job_rest_url}queue/${queueId}/`;
|
||||
this.activityService.removeItem(self);
|
||||
}
|
||||
}
|
||||
queueEnter(event) {
|
||||
const queueId = event.job_run_queueId;
|
||||
const self = `${event.blueocean_job_rest_url}queue/${queueId}/`;
|
||||
const id = this.activityService.getExpectedBuildNumber(event);
|
||||
|
||||
const runSelf = `${event.blueocean_job_rest_url}runs/${id}/`;
|
||||
|
||||
const newRun = {
|
||||
id,
|
||||
_links: {
|
||||
self: {
|
||||
href: runSelf,
|
||||
},
|
||||
parent: {
|
||||
href: event.blueocean_job_rest_url,
|
||||
},
|
||||
},
|
||||
job_run_queueId: queueId,
|
||||
pipeline: event.blueocean_job_branch_name,
|
||||
result: 'UNKNOWN',
|
||||
state: 'QUEUED',
|
||||
_item: {
|
||||
_links: {
|
||||
self: {
|
||||
href: self,
|
||||
},
|
||||
parent: {
|
||||
href: event.blueocean_job_rest_url,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
this.activityService.setItem(newRun);
|
||||
const key = this.activityService.pagerKey(event.jenkins_org, event.blueocean_job_pipeline_name);
|
||||
const pager = this.pagerService.getPager({ key });
|
||||
if (pager) {
|
||||
pager.insert(self);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { observable, action } from 'mobx';
|
||||
|
||||
/**
|
||||
* Stores the previous and current pathnames.
|
||||
*/
|
||||
export default class LocationService {
|
||||
@observable current;
|
||||
@observable previous;
|
||||
|
||||
@action setCurrent(newLocation) {
|
||||
if (newLocation.action !== 'REPLACE') {
|
||||
this.current = this.previous;
|
||||
}
|
||||
|
||||
this.current = newLocation.pathname;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
import { observable, action, computed } from 'mobx';
|
||||
import { Fetch } from '../fetch';
|
||||
|
||||
/**
|
||||
* Provide a pagination function for the generic
|
||||
* blueocean pagination
|
||||
*
|
||||
* @export
|
||||
* @param {string} url - Base url to paginate.
|
||||
* @returns {function} - Function that provides pagincated urls.
|
||||
*/
|
||||
export function paginateUrl(url) {
|
||||
const sep = url.indexOf('?') >= 0 ? '&' : '?';
|
||||
return (start, limit) => `${url}${sep}start=${start}&limit=${limit}`;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The pager fetches pages of data from the BlueOcean api. It fetches pages of data, then
|
||||
* inserts them into the [@link BunkerService], and stores the href from the data.
|
||||
*
|
||||
* MobX computes a data field from the hrefs backed by the backend cache. This allows for SSE events
|
||||
* to be proporgated to the pager.
|
||||
*
|
||||
* @export
|
||||
* @class Pager
|
||||
*/
|
||||
export class Pager {
|
||||
/**
|
||||
* List of deisplayed items hrefs.
|
||||
*/
|
||||
@observable hrefs = [];
|
||||
/**
|
||||
* pager is fetching data.
|
||||
*/
|
||||
@observable pending = false;
|
||||
/**
|
||||
* Will be set in an error occurs.
|
||||
*/
|
||||
@observable error = null;
|
||||
/**
|
||||
* The latest page the pager has fetched.
|
||||
*/
|
||||
@observable currentPage = 0;
|
||||
/**
|
||||
* More pages to fetch.
|
||||
*/
|
||||
@observable hasMore = true;
|
||||
|
||||
/**
|
||||
* Mobx computed value that creates an array of objects from the list of hrefs stored. If either the
|
||||
* bunker changes, or the hrefs change, this is recalculated and will trigger a react reaction.
|
||||
*
|
||||
* If item does not exist in bunker, then we just ignore it.
|
||||
* @readonly
|
||||
* @type {Array<Object>}
|
||||
*/
|
||||
@computed
|
||||
get data() {
|
||||
return this.hrefs.map(href => this.bunker.getItem(href)).filter(item => item !== undefined);
|
||||
}
|
||||
/**
|
||||
* Creates an instance of Pager and fetches the first page.
|
||||
*
|
||||
* @param {string} url - Base url of collectin to fetch
|
||||
* @param {number} pageSize - Page size to fetch during one load.
|
||||
* @param {BunkerService} bunker - Data store
|
||||
* @param {UrlProvider} [urlProvider=paginateUrl]
|
||||
*/
|
||||
constructor(url, pageSize, bunker, urlProvider = paginateUrl) {
|
||||
this.pageSize = pageSize;
|
||||
this.url = url;
|
||||
this.urlProvider = urlProvider;
|
||||
this.pagedUrl = this.urlProvider(url);
|
||||
this.pageSize = pageSize;
|
||||
this.bunker = bunker;
|
||||
|
||||
// Fetch the first page so that the user does not have to.
|
||||
this.fetchNextPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the next page from the backend.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
@action
|
||||
fetchNextPage() {
|
||||
// Get the next page's url.'
|
||||
const url = this.pagedUrl(this.currentPage * this.pageSize, this.pageSize + 1);
|
||||
|
||||
this.pending = true;
|
||||
|
||||
return Fetch.fetchJSON(url)
|
||||
.then(action('Process pager data', data => {
|
||||
// Store item in bunker.
|
||||
const saved = this.bunker.setItems(data);
|
||||
|
||||
// 1 extra item is fetched because need to know if there are more packages. So
|
||||
// slice off the last item, then map all items to just be hrefs.
|
||||
const trimmedHrefs = saved.slice(0, this.pageSize).map(item => item._links.self.href);
|
||||
|
||||
// Append the new Hrefs to the existing ones.
|
||||
this.hrefs = this.hrefs.concat(trimmedHrefs);
|
||||
|
||||
// True if we fetch more items than the page size.
|
||||
this.hasMore = data.length > this.pageSize;
|
||||
this.currentPage = this.currentPage + 1;
|
||||
this.pending = false;
|
||||
})).catch(err => {
|
||||
console.error('Error fetching page', err);
|
||||
action('set error', () => { this.error = err; });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the Hrefs for the pager. It also stores the latest data in the [@link BunkerService]
|
||||
*
|
||||
* This might be called if something like sorting of a list changes.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
@action
|
||||
refresh() {
|
||||
const url = this.pagedUrl(0, this.currentPage * this.pageSize + 1);
|
||||
this.pending = true;
|
||||
return Fetch.fetchJSON(url) // Fetch data
|
||||
.then(action('set data', data => {
|
||||
this.bunker.setItems(data);
|
||||
this.hrefs = data.slice(0, this.pageSize).map(x => x._links.self.href);
|
||||
this.hasMore = data.length > this.pageSize;
|
||||
this.currentPage = this.currentPage + 1;
|
||||
this.pending = false;
|
||||
})).catch(err => {
|
||||
console.error('Error fetching page', err);
|
||||
this.err = err;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts an href into the list. This will cause a reaction render for the paged list of data.
|
||||
*
|
||||
* @param {string} href - href of item to display
|
||||
* @param {number} [pos=0] - Position to insert it. Default is first item.
|
||||
*/
|
||||
@action
|
||||
insert(href, pos = 0) {
|
||||
this.hrefs.splice(pos, 0, href);
|
||||
}
|
||||
|
||||
/**
|
||||
* Href exists in pager.
|
||||
*
|
||||
* @param {string} href
|
||||
* @returns {boolean} - True if this pager does have this href
|
||||
*/
|
||||
has(href) {
|
||||
return this.hrefs.indexOf(href) > -1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* This service manages the various instances of pagers that currently exist.
|
||||
*
|
||||
* TODO: Currently a new pager is created for any new list of items to be paged. Cleanup may be
|
||||
* required to stop memory leakes. However Pagers don't store more data so this may not be an issue.'
|
||||
*
|
||||
* @export
|
||||
* @class PagerService
|
||||
*/
|
||||
export class PagerService {
|
||||
/**
|
||||
* MobX map to hold [@link Pager]'s'
|
||||
*/
|
||||
_pagerMap = new Map();
|
||||
|
||||
/**
|
||||
* Registers a pager with the PagerService.
|
||||
*
|
||||
* Namespacing strings is prefered to stop colisions. E.g. Activity/$org-$pipeline.
|
||||
*
|
||||
* @param {any} key - Key to register the pager under.
|
||||
* @param {Pager} pager - pager to regiser.
|
||||
*/
|
||||
registerPager(key, pager) {
|
||||
if (this._pagerMap.has(key)) {
|
||||
throw new Error(`Pager '${key}' already exits in PagerService`);
|
||||
}
|
||||
this._pagerMap.set(key, pager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes pager from the cache.
|
||||
*
|
||||
* @param {any} key
|
||||
*/
|
||||
removePager(key) {
|
||||
if (this._pagerMap.has(key)) {
|
||||
this._pagerMap.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily creates a pager. Do this because pager fetches the first page when it is created.
|
||||
*
|
||||
* @callback lazyPager
|
||||
* @returns {Pager}
|
||||
*/
|
||||
/**
|
||||
* Gets a pager from the cache.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {any} options.key - Key to store pager under.
|
||||
* @param {lazyPager} options.lazyPager - function to lazily crete the pager.
|
||||
* @returns {Pager}
|
||||
*/
|
||||
getPager({ key, lazyPager }) {
|
||||
if (this._pagerMap.has(key)) {
|
||||
return this._pagerMap.get(key);
|
||||
}
|
||||
if (lazyPager) {
|
||||
const pager = lazyPager();
|
||||
this.registerPager(key, pager);
|
||||
return pager;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refetches the list of items the pagers that use a specific [@link BunkerService] to display.
|
||||
* This is done in the case of reordering.
|
||||
*
|
||||
* TODO: Make this more targetted.
|
||||
*
|
||||
* @param {BunkerService} bunkerService A service that extends [@link BunkerService]
|
||||
*/
|
||||
refresh(bunkerService) {
|
||||
this._pagerMap.forEach(pager => {
|
||||
if (bunkerService === pager.bunker) {
|
||||
pager.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all pagers for a [@link BunkerService]
|
||||
*
|
||||
* @param {BunkerService} bunker
|
||||
* @returns {Pager[]}
|
||||
*/
|
||||
getPagers(bunker) {
|
||||
const ret = [];
|
||||
this._pagerMap.forEach(pager => {
|
||||
if (bunker === pager.bunker) {
|
||||
ret.push(bunker);
|
||||
}
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import { Pager } from './Pager';
|
||||
import RestPaths from '../paths/rest';
|
||||
import { Fetch } from '../fetch';
|
||||
import utils from '../utils';
|
||||
import { BunkerService } from './BunkerService';
|
||||
import { action } from 'mobx';
|
||||
/**
|
||||
* This class handles pipeline related data. This includes pipelines, branches and pullrequeusts as they are
|
||||
* all pipelines in the backend.
|
||||
*
|
||||
* @export
|
||||
* @class PipelineService
|
||||
* @extends {BunkerService}
|
||||
*/
|
||||
export class PipelineService extends BunkerService {
|
||||
/**
|
||||
* Creates an instance of PipelineService.
|
||||
*
|
||||
* @param {PagerService} pagerService
|
||||
* @param {ActivityService} activityService
|
||||
*/
|
||||
constructor(pagerService, activityService) {
|
||||
super(pagerService);
|
||||
this.activityService = activityService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets pager for /blue/pipelines
|
||||
*
|
||||
* @returns {Pager}
|
||||
*/
|
||||
allPipelinesPager() {
|
||||
return this.pagerService.getPager({
|
||||
key: 'PipelinesAll',
|
||||
lazyPager: () => new Pager(RestPaths.allPipelines(), 25, this),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets pager for /blue/organization/:organization/pipelines
|
||||
*
|
||||
* @param {strinb} organization organization pager belongs to.
|
||||
* @returns {Pager}
|
||||
*/
|
||||
organiztionPipelinesPager(organization) {
|
||||
return this.pagerService.getPager({
|
||||
key: `Pipelines/${organization}`,
|
||||
lazyPager: () => new Pager(RestPaths.organizationPipelines(organization), 25, this),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets pager for /blue/organization/:organization/pipelines/:pipeline/branches
|
||||
*
|
||||
* @param {string} organization
|
||||
* @param {string} pipeline
|
||||
* @returns {Pager}
|
||||
*/
|
||||
branchPager(organization: string, pipeline: string) {
|
||||
return this.pagerService.getPager({
|
||||
key: `Branches/${organization}-${pipeline}`,
|
||||
lazyPager: () => new Pager(RestPaths.branches(organization, pipeline), 25, this),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets pager for /blue/organization/:organization/pipelines/:pipeline/pullRequests
|
||||
*
|
||||
* @param {string} organization
|
||||
* @param {string} pipeline
|
||||
* @returns {Pager}
|
||||
*/
|
||||
prPager(organization: string, pipeline: string) {
|
||||
return this.pagerService.getPager({
|
||||
key: `PRs/${organization}-${pipeline}`,
|
||||
lazyPager: () => new Pager(RestPaths.pullRequests(organization, pipeline), 25, this),
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Adds the latest run to the [@link ActivityService], and sets the latestRun as a mobx computed value.
|
||||
*
|
||||
* @param {Object} pipelineData Raw data from backend.
|
||||
* @return {Object} mapped pipelineData with latestRun set to be a mobx computed value.
|
||||
*/
|
||||
bunkerMapper = (pipelineData) => {
|
||||
const data = utils.clone(pipelineData);
|
||||
const latestRun = data.latestRun;
|
||||
|
||||
if (latestRun) {
|
||||
data.latestRun = this.activityService.setItem(latestRun);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
/**
|
||||
* Gets a pipeline from the store
|
||||
*
|
||||
* @param {string} href - Self href of the pipeline.
|
||||
* @returns {Object} - Mobx computed value of the pipeline.
|
||||
*/
|
||||
getPipeline(href) {
|
||||
return this.getItem(href);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetches pipeline from the backend and stores it in
|
||||
*
|
||||
* @param {string} href - Self href of the pipeline.
|
||||
* @param {Object} options
|
||||
* @param {boolean} options.useCache - If true fetch from the store if it exists.
|
||||
* @returns
|
||||
*/
|
||||
fetchPipeline(href, { useCache } = {}) {
|
||||
if (useCache && this.hasItem(href)) {
|
||||
return Promise.resolve(this.getItem(href));
|
||||
}
|
||||
|
||||
return Fetch.fetchJSON(href).then(data => this.setItem(data));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* MobX Action to update the latest run on a pipeline. Use for SSE. This will cause a reaction
|
||||
* and rerender anything that uses the latest run of this pipeline.
|
||||
*
|
||||
* @param {Object} run An activity from activityService.getItem().
|
||||
*/
|
||||
@action
|
||||
updateLatestRun(run) {
|
||||
const pipeline = this.getItem(run._links.parent.href);
|
||||
if (pipeline) {
|
||||
pipeline.latestRun = run;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
export class SSEService {
|
||||
constructor(connection) {
|
||||
this.connection = connection;
|
||||
this._handlers = [];
|
||||
}
|
||||
|
||||
_initListeners() {
|
||||
if (!this.jobListener) {
|
||||
this.jobListener = this.connection.subscribe('job', (event) => {
|
||||
this._handleJobEvent(event);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerHandler(handlerFn) {
|
||||
this._handlers.push(handlerFn);
|
||||
}
|
||||
_handleJobEvent(event) {
|
||||
this._handlers.forEach(handler => handler(event));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// @flow
|
||||
export { PagerService } from './PagerService';
|
||||
export { PipelineService } from './PipelineService';
|
||||
export { SSEService } from './SSEService';
|
||||
export { Pager } from './Pager';
|
||||
export { ActivityService } from './ActivityService';
|
||||
export { DefaultSSEHandler } from './DefaultSSEHandler';
|
||||
export LocationService from './LocationService';
|
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* Created by cmeyers on 7/29/16.
|
||||
*/
|
||||
import defaultFetch from 'isomorphic-fetch';
|
||||
import config from '../urlconfig';
|
||||
import utils from '../utils';
|
||||
|
||||
/**
|
||||
* Wraps the SSE Gateway and fetches data related to events from REST API.
|
||||
*/
|
||||
export class SseBus {
|
||||
|
||||
constructor(connection, fetch) {
|
||||
this.id = this._random();
|
||||
this.connection = connection;
|
||||
this.fetch = fetch || defaultFetch;
|
||||
this.externalListeners = {};
|
||||
this.sseListeners = {};
|
||||
}
|
||||
|
||||
dispose() {
|
||||
Object.keys(this.sseListeners).forEach((token) => {
|
||||
this.unsubscribe(token);
|
||||
});
|
||||
|
||||
this.externalListeners = {};
|
||||
this.sseListeners = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to job events.
|
||||
* @param callback func to invoke with job data
|
||||
* @param jobFilter func invoked for each job event, return false to suppress callback invocation
|
||||
* @returns {number} unsubscribe token
|
||||
*/
|
||||
subscribeToJob(callback, jobFilter) {
|
||||
const id = this._random();
|
||||
|
||||
this.externalListeners[id] = {
|
||||
listener: callback,
|
||||
filter: jobFilter,
|
||||
};
|
||||
|
||||
if (!this.sseListeners.job) {
|
||||
const sseListener = this.connection.subscribe('job', (event) => {
|
||||
this._handleJobEvent(event);
|
||||
});
|
||||
|
||||
this.sseListeners.job = sseListener;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
unsubscribe(token) {
|
||||
delete this.externalListeners[token];
|
||||
|
||||
if (Object.keys(this.externalListeners).length === 0) {
|
||||
this.connection.unsubscribe(this.sseListeners.job);
|
||||
delete this.sseListeners.job;
|
||||
}
|
||||
}
|
||||
|
||||
_handleJobEvent(event) {
|
||||
const subscriptions = Object
|
||||
.keys(this.externalListeners)
|
||||
.map(subId => this.externalListeners[subId]);
|
||||
|
||||
const interestedListeners = subscriptions
|
||||
.filter(sub => sub.filter(event))
|
||||
.map(sub => sub.listener);
|
||||
|
||||
// if no filters are interested in the event, bail
|
||||
if (interestedListeners.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.jenkins_event) {
|
||||
case 'job_crud_created':
|
||||
case 'job_crud_deleted':
|
||||
case 'job_crud_renamed':
|
||||
this._refetchPipelines();
|
||||
break;
|
||||
case 'job_run_queue_buildable':
|
||||
case 'job_run_queue_enter':
|
||||
this._enqueueJob(event, interestedListeners);
|
||||
break;
|
||||
case 'job_run_queue_left':
|
||||
case 'job_run_queue_blocked': {
|
||||
break;
|
||||
}
|
||||
case 'job_run_paused':
|
||||
case 'job_run_unpaused':
|
||||
case 'job_run_started': {
|
||||
this._updateJob(event, interestedListeners);
|
||||
break;
|
||||
}
|
||||
case 'job_run_ended': {
|
||||
this._updateJob(event, interestedListeners);
|
||||
break;
|
||||
}
|
||||
default :
|
||||
// Else ignore the event.
|
||||
}
|
||||
}
|
||||
|
||||
_refetchPipelines() {
|
||||
// TODO: implement once migration into commons JS
|
||||
}
|
||||
|
||||
_enqueueJob(event, listeners) {
|
||||
const queuedRun = {};
|
||||
|
||||
queuedRun.pipeline = event.job_ismultibranch ?
|
||||
event.blueocean_job_branch_name :
|
||||
event.blueocean_job_pipeline_name;
|
||||
|
||||
const runUrl = utils.cleanSlashes(`${event.blueocean_job_rest_url}/runs/${event.job_run_queueId}`);
|
||||
|
||||
queuedRun._links = {
|
||||
self: {
|
||||
href: runUrl,
|
||||
},
|
||||
};
|
||||
|
||||
queuedRun.state = 'QUEUED';
|
||||
queuedRun.result = 'UNKNOWN';
|
||||
|
||||
for (const listener of listeners) {
|
||||
listener(queuedRun, event);
|
||||
}
|
||||
}
|
||||
|
||||
_updateJob(event, listeners) {
|
||||
const baseUrl = config.getJenkinsRootURL();
|
||||
const url = utils.cleanSlashes(`${baseUrl}/${event.blueocean_job_rest_url}/runs/${event.jenkins_object_id}`);
|
||||
|
||||
this.fetch(url)
|
||||
.then((data) => {
|
||||
const updatedRun = utils.clone(data);
|
||||
|
||||
// FIXME: Talk to CMeyers why we cannot use the data.state?
|
||||
// in many cases the SSE and subsequent REST call occur so quickly
|
||||
// that the run's state is stale. force the state to the correct value.
|
||||
if (event.jenkins_event === 'job_run_ended') {
|
||||
updatedRun.state = 'FINISHED';
|
||||
} else if (event.jenkins_event === 'job_run_paused') {
|
||||
updatedRun.state = 'PAUSED';
|
||||
} else {
|
||||
updatedRun.state = 'RUNNING';
|
||||
}
|
||||
|
||||
for (const listener of listeners) {
|
||||
listener(updatedRun, event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_updateMultiBranchPipelineBranches() {
|
||||
// TODO: implement once migration into commons JS
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
* @private
|
||||
*/
|
||||
_random() {
|
||||
return Math.random() * Math.pow(10, 16);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2017, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Client-side local storage for Blue Ocean.
|
||||
* See https://tfennelly.github.io/jenkins-js-storage/
|
||||
*/
|
||||
|
||||
import * as storage from '@jenkins-cd/storage';
|
||||
import { blueocean } from './scopes';
|
||||
import config from './config';
|
||||
|
||||
export const jenkinsNS = storage.jenkinsNamespace();
|
||||
export const installInfo = jenkinsNS.subspace('installInfo');
|
||||
|
||||
/**
|
||||
* Check do we need to clear the jenkinsNS.
|
||||
* <p>
|
||||
* Simple process of checking if the version OR list of plugins
|
||||
* stored (from the last change) have changed based on what was
|
||||
* delivered with this page.
|
||||
* <p>
|
||||
* Internal use only. Exported for testing purposes only.
|
||||
* @param {string} installVersion The version of the Jenkins instance that's running now
|
||||
* i.e. that loaded this page.
|
||||
* @param {Array} installPluginList The list of active plugins installed in the Jenkins instance
|
||||
* that's running now i.e. that loaded this page.
|
||||
* @private
|
||||
*/
|
||||
export const _clearJenkinsNS = (installVersion, installPluginList) => {
|
||||
// Info about the Jenkins that was running the last time we loaded this page.
|
||||
const storedVersion = installInfo.get('version');
|
||||
const storedPluginList = installInfo.get('plugins');
|
||||
|
||||
const doClear = (because) => {
|
||||
jenkinsNS.clear();
|
||||
installInfo.set('version', installVersion);
|
||||
installInfo.set('plugins', installPluginList);
|
||||
installInfo.set('lastcleared', {
|
||||
at: Date.now(),
|
||||
because,
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
if (storedVersion && storedPluginList) {
|
||||
// compare the Jenkins version
|
||||
if (installVersion !== storedVersion) {
|
||||
doClear(`Jenkins versions did not match. installVersion: ${installVersion}, storedVersion: ${storedVersion}`);
|
||||
} else {
|
||||
// compare the plugin lists
|
||||
if (installPluginList.length !== storedPluginList.length) {
|
||||
// Different number of active plugins.
|
||||
// No need to check the names and versions.
|
||||
doClear('Different number of active plugins');
|
||||
} else {
|
||||
// Same number of plugins. Lets check that they all
|
||||
// match up i.e. that we can find each plugin in each list and
|
||||
// that the versions match.
|
||||
try {
|
||||
installPluginList.forEach((installedPlugin) => {
|
||||
let found = false;
|
||||
storedPluginList.forEach((storedPlugin) => {
|
||||
if (storedPlugin.hpiPluginId === installedPlugin.hpiPluginId) {
|
||||
// same plugin.
|
||||
found = true;
|
||||
// Check the versions.
|
||||
if (storedPlugin.hpiPluginVer !== installedPlugin.hpiPluginVer) {
|
||||
throw new Error(`Different plugin versions for plugin ${installedPlugin.hpiPluginId}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!found) {
|
||||
throw new Error(`New plugin installed ${installedPlugin.hpiPluginId}`);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// One of the plugins has been updated or removed.
|
||||
// See Errors thrown inside above try/catch.
|
||||
doClear(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Theoretically no need to clear in this case,
|
||||
// but lets do it anyway.
|
||||
doClear('No Jenkins info stored. Clearing anyway, just in case.');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Unexpected error while checking/clearing Jenkins instance client-side storage. Clearing as a precaution.', e);
|
||||
doClear(`Unexpected error while checking/clearing Jenkins instance client-side storage: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Call the clear function automatically.
|
||||
const installVersion = config.getJenkinsConfig().version;
|
||||
const installPluginList = blueocean.jsExtensions;
|
||||
if (installVersion && installPluginList) {
|
||||
_clearJenkinsNS(installVersion, installPluginList);
|
||||
} else {
|
||||
console.warn('Unexpected state. Blue Ocean preload state not on page as expected. This is okay if running in a test.');
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import { Fetch, FetchFunctions } from './fetch';
|
||||
|
||||
// default impls
|
||||
const fetchJSON = Fetch.fetchJSON;
|
||||
const fetch = Fetch.fetch;
|
||||
|
||||
export default {
|
||||
|
||||
/**
|
||||
* Switches fetch functions with arbitrary replacements.
|
||||
* Useful for test spies.
|
||||
*
|
||||
* @param _fetchJSON
|
||||
* @param _fetch
|
||||
*/
|
||||
patchFetch(_fetchJSON, _fetch) {
|
||||
Fetch.fetchJSON = _fetchJSON;
|
||||
Fetch.fetch = _fetch;
|
||||
},
|
||||
|
||||
/**
|
||||
* Switches fetch functions for ones that dont use JWT. Needed
|
||||
* for running tests.
|
||||
*/
|
||||
patchFetchNoJWT() {
|
||||
Fetch.fetchJSON = FetchFunctions.rawFetchJSON;
|
||||
Fetch.fetch = FetchFunctions.rawFetch;
|
||||
},
|
||||
|
||||
/**
|
||||
* Restores original fetch functions.
|
||||
*/
|
||||
restoreFetch() {
|
||||
Fetch.fetchJSON = fetchJSON;
|
||||
Fetch.fetch = fetch;
|
||||
},
|
||||
|
||||
/**
|
||||
* Patches fetch functions with a resolved promise. This will make all fetch calls return
|
||||
* this data.
|
||||
*
|
||||
* Usage
|
||||
*
|
||||
* TestUtils.patchFetchWithData((url, options) => {
|
||||
* assert.equals(url,"someurl")
|
||||
* return { mydata: 5 }
|
||||
* })
|
||||
*/
|
||||
patchFetchWithData(dataFn) {
|
||||
Fetch.fetchJSON = Fetch.fetch = (url, options) => {
|
||||
const { onSuccess, onError } = options || {};
|
||||
|
||||
const data = Promise.resolve(dataFn(url, options));
|
||||
|
||||
if (onSuccess) {
|
||||
return data.then(onSuccess).catch(FetchFunctions.onError(onError));
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
},
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
let blueOceanAppURL = '/';
|
||||
let jenkinsRootURL = '';
|
||||
|
||||
let loaded = false;
|
||||
|
||||
function loadConfig() {
|
||||
try {
|
||||
const headElement = document.getElementsByTagName('head')[0];
|
||||
|
||||
// Look up where the Blue Ocean app is hosted
|
||||
blueOceanAppURL = headElement.getAttribute('data-appurl');
|
||||
if (typeof blueOceanAppURL !== 'string') {
|
||||
blueOceanAppURL = '/';
|
||||
}
|
||||
|
||||
jenkinsRootURL = headElement.getAttribute('data-rooturl');
|
||||
loaded = true;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('error reading attributes from document; urls will be empty', error);
|
||||
|
||||
loaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getJenkinsRootURL() {
|
||||
if (!loaded) {
|
||||
loadConfig();
|
||||
}
|
||||
return jenkinsRootURL;
|
||||
},
|
||||
|
||||
getBlueOceanAppURL() {
|
||||
if (!loaded) {
|
||||
loadConfig();
|
||||
}
|
||||
return blueOceanAppURL;
|
||||
},
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
// @flow
|
||||
|
||||
/**
|
||||
* Trims duplicate forward slashes to a single slash and adds trailing slash if needed.
|
||||
* @param url
|
||||
* @returns {string}
|
||||
*/
|
||||
const cleanSlashes = (url: string) => {
|
||||
if (url.indexOf('//') !== -1) {
|
||||
let cleanUrl = url.replace('//', '/');
|
||||
cleanUrl = cleanUrl.substr(-1) === '/' ?
|
||||
cleanUrl : `${cleanUrl}/`;
|
||||
|
||||
return cleanSlashes(cleanUrl);
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
|
||||
export default {
|
||||
cleanSlashes,
|
||||
clone(obj: Object) {
|
||||
if (!obj) return obj;
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
},
|
||||
windowOrGlobal() {
|
||||
return (typeof self === 'object' && self.self === self && self) ||
|
||||
(typeof global === 'object' && global.global === global && global) ||
|
||||
this;
|
||||
},
|
||||
refreshPage() {
|
||||
if (this.windowOrGlobal().location.reload) {
|
||||
this.windowOrGlobal().location.reload(true);
|
||||
}
|
||||
},
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
import { Promise } from 'es6-promise';
|
||||
|
||||
/**
|
||||
* DuplicateCallTracker maintains active calls against a particular key
|
||||
*/
|
||||
export class DeDupeCallTracker {
|
||||
constructor() {
|
||||
/**
|
||||
* Onload callbacks cache. Used to ensure we don't
|
||||
* issue multiple in-parallel requests for the same
|
||||
* class metadata.
|
||||
*/
|
||||
this.promises = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generalization of duplicate request consolidation:
|
||||
*
|
||||
* @key: key to use to track the duplicate requests
|
||||
* @promiseCreator: function that will return an initial promise, e.g. () => fetch(...)
|
||||
* @return a Promise
|
||||
*/
|
||||
dedupe(key, promiseCreator) {
|
||||
// get active or create
|
||||
return this.promises[key] || (this.promises[key] =
|
||||
promiseCreator()
|
||||
.then((data) => {
|
||||
delete this.promises[key];
|
||||
return data;
|
||||
})
|
||||
.catch((err) => {
|
||||
delete this.promises[key];
|
||||
return Promise.reject(err);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const deDupeCallTracker = new DeDupeCallTracker();
|
||||
|
||||
/**
|
||||
* Generalization of duplicate request consolidation:
|
||||
*
|
||||
* @key: key to use to track the duplicate requests
|
||||
* @promiseCreator: function that will return an initial promise, e.g. () => fetch(...)
|
||||
* @return a Promise
|
||||
*/
|
||||
export default function dedupe(key, promiseCreator) {
|
||||
return deDupeCallTracker.dedupe(key, promiseCreator);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
@import 'variables';
|
||||
@import 'replay-button';
|
||||
@import 'run-button';
|
|
@ -0,0 +1,64 @@
|
|||
.replay-button-component {
|
||||
|
||||
display: inline-block;
|
||||
|
||||
.replay-button {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// default (text) display
|
||||
.svg-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// icon display
|
||||
&.icon-button {
|
||||
height: 24px;
|
||||
|
||||
.replay-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
position: relative;
|
||||
|
||||
.svg-icon {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.button-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// default (light) themeing
|
||||
.svg-icon {
|
||||
fill: @brand-primary;
|
||||
|
||||
&:hover {
|
||||
fill: @brand-primary-dark;
|
||||
}
|
||||
}
|
||||
|
||||
// dark themeing
|
||||
&.dark {
|
||||
.btn {
|
||||
background: transparent;
|
||||
color: white;
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
fill: white;
|
||||
|
||||
&:hover {
|
||||
fill: #CCC;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
.run-button-component {
|
||||
|
||||
display: inline-block;
|
||||
|
||||
.run-button, .stop-button {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// default (text) display
|
||||
.svg-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// icon display
|
||||
&.icon-button {
|
||||
height: 24px;
|
||||
|
||||
.run-button, .stop-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
position: relative;
|
||||
|
||||
.svg-icon {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.stop-button {
|
||||
/* asset has different padding than material-icon asset used in .run-button: nudge it slightly */
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
|
||||
&.stopping {
|
||||
opacity: 0.4;
|
||||
cursor: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.button-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// default (light) themeing
|
||||
.svg-icon, .svg-icon-inner {
|
||||
fill: @brand-primary;
|
||||
}
|
||||
|
||||
.svg-icon:hover {
|
||||
fill: @brand-primary-dark;
|
||||
|
||||
.svg-icon-inner {
|
||||
fill: @brand-primary-dark;
|
||||
}
|
||||
}
|
||||
|
||||
// dark themeing
|
||||
&.dark {
|
||||
.btn {
|
||||
background: transparent;
|
||||
color: white;
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.svg-icon, .svg-icon-inner {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.svg-icon:hover {
|
||||
fill: #CCC;
|
||||
|
||||
.svg-icon-inner {
|
||||
fill: #CCC;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue