Merge "Add undefined check for project"
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
index 956a94d..ad3443b 100644
--- a/Documentation/js_licenses.txt
+++ b/Documentation/js_licenses.txt
@@ -326,227 +326,49 @@
----
-[[font-roboto-local-fonts-robotomono]]
-font-roboto-local-fonts-robotomono
+[[Polymer-2017]]
+Polymer-2017
-* @polymer/font-roboto-local - only the following file(s):
-** fonts/robotomono/LICENSE.txt
-** fonts/robotomono/METADATA.json
-** fonts/robotomono/RobotoMono-Bold.ttf
-** fonts/robotomono/RobotoMono-BoldItalic.ttf
-** fonts/robotomono/RobotoMono-Italic.ttf
-** fonts/robotomono/RobotoMono-Light.ttf
-** fonts/robotomono/RobotoMono-LightItalic.ttf
-** fonts/robotomono/RobotoMono-Medium.ttf
-** fonts/robotomono/RobotoMono-MediumItalic.ttf
-** fonts/robotomono/RobotoMono-Regular.ttf
-** fonts/robotomono/RobotoMono-Thin.ttf
-** fonts/robotomono/RobotoMono-ThinItalic.ttf
+* @polymer/decorators
+* @polymer/polymer
+* @webcomponents/shadycss
-[[font-roboto-local-fonts-robotomono_license]]
+[[Polymer-2017_license]]
----
+Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
- 1. Definitions.
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
----
@@ -945,48 +767,227 @@
----
-[[Polymer-2017]]
-Polymer-2017
+[[font-roboto-local-fonts-robotomono]]
+font-roboto-local-fonts-robotomono
-* @polymer/polymer
-* @webcomponents/shadycss
+* @polymer/font-roboto-local - only the following file(s):
+** fonts/robotomono/LICENSE.txt
+** fonts/robotomono/METADATA.json
+** fonts/robotomono/RobotoMono-Bold.ttf
+** fonts/robotomono/RobotoMono-BoldItalic.ttf
+** fonts/robotomono/RobotoMono-Italic.ttf
+** fonts/robotomono/RobotoMono-Light.ttf
+** fonts/robotomono/RobotoMono-LightItalic.ttf
+** fonts/robotomono/RobotoMono-Medium.ttf
+** fonts/robotomono/RobotoMono-MediumItalic.ttf
+** fonts/robotomono/RobotoMono-Regular.ttf
+** fonts/robotomono/RobotoMono-Thin.ttf
+** fonts/robotomono/RobotoMono-ThinItalic.ttf
-[[Polymer-2017_license]]
+[[font-roboto-local-fonts-robotomono_license]]
----
-Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
-This code may only be used under the BSD style license found at
-http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
-http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
-found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
-part of the polymer project is also subject to an additional IP rights grant
-found at http://polymer.github.io/PATENTS.txt
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
- * Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above
-copyright notice, this list of conditions and the following disclaimer
-in the documentation and/or other materials provided with the
-distribution.
- * Neither the name of Google Inc. nor the names of its
-contributors may be used to endorse or promote products derived from
-this software without specific prior written permission.
+ 1. Definitions.
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
----
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index d561596..98e99d4 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -3268,227 +3268,49 @@
----
-[[font-roboto-local-fonts-robotomono]]
-font-roboto-local-fonts-robotomono
+[[Polymer-2017]]
+Polymer-2017
-* @polymer/font-roboto-local - only the following file(s):
-** fonts/robotomono/LICENSE.txt
-** fonts/robotomono/METADATA.json
-** fonts/robotomono/RobotoMono-Bold.ttf
-** fonts/robotomono/RobotoMono-BoldItalic.ttf
-** fonts/robotomono/RobotoMono-Italic.ttf
-** fonts/robotomono/RobotoMono-Light.ttf
-** fonts/robotomono/RobotoMono-LightItalic.ttf
-** fonts/robotomono/RobotoMono-Medium.ttf
-** fonts/robotomono/RobotoMono-MediumItalic.ttf
-** fonts/robotomono/RobotoMono-Regular.ttf
-** fonts/robotomono/RobotoMono-Thin.ttf
-** fonts/robotomono/RobotoMono-ThinItalic.ttf
+* @polymer/decorators
+* @polymer/polymer
+* @webcomponents/shadycss
-[[font-roboto-local-fonts-robotomono_license]]
+[[Polymer-2017_license]]
----
+Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
- 1. Definitions.
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
----
@@ -3887,48 +3709,227 @@
----
-[[Polymer-2017]]
-Polymer-2017
+[[font-roboto-local-fonts-robotomono]]
+font-roboto-local-fonts-robotomono
-* @polymer/polymer
-* @webcomponents/shadycss
+* @polymer/font-roboto-local - only the following file(s):
+** fonts/robotomono/LICENSE.txt
+** fonts/robotomono/METADATA.json
+** fonts/robotomono/RobotoMono-Bold.ttf
+** fonts/robotomono/RobotoMono-BoldItalic.ttf
+** fonts/robotomono/RobotoMono-Italic.ttf
+** fonts/robotomono/RobotoMono-Light.ttf
+** fonts/robotomono/RobotoMono-LightItalic.ttf
+** fonts/robotomono/RobotoMono-Medium.ttf
+** fonts/robotomono/RobotoMono-MediumItalic.ttf
+** fonts/robotomono/RobotoMono-Regular.ttf
+** fonts/robotomono/RobotoMono-Thin.ttf
+** fonts/robotomono/RobotoMono-ThinItalic.ttf
-[[Polymer-2017_license]]
+[[font-roboto-local-fonts-robotomono_license]]
----
-Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
-This code may only be used under the BSD style license found at
-http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
-http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
-found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
-part of the polymer project is also subject to an additional IP rights grant
-found at http://polymer.github.io/PATENTS.txt
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
- * Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above
-copyright notice, this list of conditions and the following disclaimer
-in the documentation and/or other materials provided with the
-distribution.
- * Neither the name of Google Inc. nor the names of its
-contributors may be used to endorse or promote products derived from
-this software without specific prior written permission.
+ 1. Definitions.
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
----
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/.eslintrc.js
index ebba3eb..bf1fcc6 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/.eslintrc.js
@@ -197,16 +197,12 @@
"jsdoc/require-param-description": 0,
// https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-name
"jsdoc/require-param-name": 2,
- // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-type
- "jsdoc/require-param-type": 2,
// https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns
"jsdoc/require-returns": 0,
// https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-check
"jsdoc/require-returns-check": 0,
// https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-description
"jsdoc/require-returns-description": 0,
- // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-type
- "jsdoc/require-returns-type": 2,
// https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-valid-types
"jsdoc/valid-types": 2,
// https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-file-overview
@@ -253,6 +249,10 @@
// .js-only rules
"files": ["**/*.js"],
"rules": {
+ // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-type
+ "jsdoc/require-param-type": 2,
+ // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-type
+ "jsdoc/require-returns-type": 2,
// The rule is required for .js files only, because typescript compiler
// always checks import.
"import/no-unresolved": 2,
diff --git a/polygerrit-ui/app/constants/constants.js b/polygerrit-ui/app/constants/constants.js
deleted file mode 100644
index 3a4cb5e..0000000
--- a/polygerrit-ui/app/constants/constants.js
+++ /dev/null
@@ -1,88 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-goog.declareModuleId('polygerrit.constants.constants');
-
-/**
- * @enum
- * @desc Tab names for primary tabs on change view page.
- */
-export const PrimaryTab = {
- FILES: 'files',
- /**
- * When renaming this, the links in UrlFormatter must be updated.
- */
- COMMENT_THREADS: 'comments',
- FINDINGS: 'findings',
-};
-
-/**
- * @enum
- * @desc Tab names for secondary tabs on change view page.
- */
-export const SecondaryTab = {
- CHANGE_LOG: '_changeLog',
-};
-
-/**
- * @enum
- * @desc Tag names of change log messages.
- */
-export const MessageTag = {
- TAG_DELETE_REVIEWER: 'autogenerated:gerrit:deleteReviewer',
- TAG_NEW_PATCHSET: 'autogenerated:gerrit:newPatchSet',
- TAG_NEW_WIP_PATCHSET: 'autogenerated:gerrit:newWipPatchSet',
- TAG_REVIEWER_UPDATE: 'autogenerated:gerrit:reviewerUpdate',
- TAG_SET_PRIVATE: 'autogenerated:gerrit:setPrivate',
- TAG_UNSET_PRIVATE: 'autogenerated:gerrit:unsetPrivate',
- TAG_SET_READY: 'autogenerated:gerrit:setReadyForReview',
- TAG_SET_WIP: 'autogenerated:gerrit:setWorkInProgress',
- TAG_SET_ASSIGNEE: 'autogenerated:gerrit:setAssignee',
- TAG_UNSET_ASSIGNEE: 'autogenerated:gerrit:deleteAssignee',
-};
-
-/**
- * @enum
- * @desc Modes for gr-diff-cursor
- * The scroll behavior for the cursor. Values are 'never' and
- * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
- * the viewport.
- */
-export const ScrollMode = {
- KEEP_VISIBLE: 'keep-visible',
- NEVER: 'never',
-};
-
-/**
- * @enum
- * @desc Specifies status for a change
- */
-export const ChangeStatus = {
- ABANDONED: 'ABANDONED',
- MERGED: 'MERGED',
- NEW: 'NEW',
-};
-
-/**
- * @enum
- * @desc Special file paths
- */
-export const SpecialFilePath = {
- PATCHSET_LEVEL_COMMENTS: '/PATCHSET_LEVEL',
- COMMIT_MESSAGE: '/COMMIT_MSG',
- MERGE_LIST: '/MERGE_LIST',
-};
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
new file mode 100644
index 0000000..0b26a30
--- /dev/null
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -0,0 +1,138 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @desc Tab names for primary tabs on change view page.
+ */
+export enum PrimaryTab {
+ FILES = 'files',
+ /**
+ * When renaming this, the links in UrlFormatter must be updated.
+ */
+ COMMENT_THREADS = 'comments',
+ FINDINGS = 'findings',
+}
+
+/**
+ * @desc Tab names for secondary tabs on change view page.
+ */
+export enum SecondaryTab {
+ CHANGE_LOG = '_changeLog',
+}
+
+/**
+ * @desc Tag names of change log messages.
+ */
+export enum MessageTag {
+ TAG_DELETE_REVIEWER = 'autogenerated:gerrit:deleteReviewer',
+ TAG_NEW_PATCHSET = 'autogenerated:gerrit:newPatchSet',
+ TAG_NEW_WIP_PATCHSET = 'autogenerated:gerrit:newWipPatchSet',
+ TAG_REVIEWER_UPDATE = 'autogenerated:gerrit:reviewerUpdate',
+ TAG_SET_PRIVATE = 'autogenerated:gerrit:setPrivate',
+ TAG_UNSET_PRIVATE = 'autogenerated:gerrit:unsetPrivate',
+ TAG_SET_READY = 'autogenerated:gerrit:setReadyForReview',
+ TAG_SET_WIP = 'autogenerated:gerrit:setWorkInProgress',
+ TAG_SET_ASSIGNEE = 'autogenerated:gerrit:setAssignee',
+ TAG_UNSET_ASSIGNEE = 'autogenerated:gerrit:deleteAssignee',
+}
+
+/**
+ * @desc Modes for gr-diff-cursor
+ * The scroll behavior for the cursor. Values are 'never' and
+ * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
+ * the viewport.
+ */
+export enum ScrollMode {
+ KEEP_VISIBLE = 'keep-visible',
+ NEVER = 'never',
+}
+
+/**
+ * @desc Specifies status for a change
+ */
+export enum ChangeStatus {
+ ABANDONED = 'ABANDONED',
+ MERGED = 'MERGED',
+ NEW = 'NEW',
+}
+
+/**
+ * @desc Special file paths
+ */
+export enum SpecialFilePath {
+ PATCHSET_LEVEL_COMMENTS = '/PATCHSET_LEVEL',
+ COMMIT_MESSAGE = '/COMMIT_MSG',
+ MERGE_LIST = '/MERGE_LIST',
+}
+
+/**
+ * @desc The reviewer state
+ */
+export enum RequirementStatus {
+ OK = 'OK',
+ NOT_READY = 'NOT_READY',
+ RULE_ERROR = 'RULE_ERROR',
+}
+
+/**
+ * @desc The reviewer state
+ */
+export enum ReviewerState {
+ REVIEWER = 'REVIEWER',
+ CC = 'CC',
+ REMOVED = 'REMOVED',
+}
+
+/**
+ * @desc The patchset kind
+ */
+export enum RevisionKind {
+ REWORK = 'REWORK',
+ TRIVIAL_REBASE = 'TRIVIAL_REBASE',
+ MERGE_FIRST_PARENT_UPDATE = 'MERGE_FIRST_PARENT_UPDATE',
+ NO_CODE_CHANGE = 'NO_CODE_CHANGE',
+ NO_CHANGE = 'NO_CHANGE',
+}
+
+/**
+ * @desc The status of fixing the problem
+ */
+export enum ProblemInfoStatus {
+ FIXED = 'FIXED',
+ FIX_FAILED = 'FIX_FAILED',
+}
+
+/**
+ * @desc The status of the file
+ */
+export enum FileInfoStatus {
+ ADDED = 'A',
+ DELETED = 'D',
+ RENAMED = 'R',
+ COPIED = 'C',
+ REWRITTEN = 'W',
+ // Modifed = 'M', but API not set it if the file was modified
+}
+
+/**
+ * @desc The status of the file
+ */
+export enum GpgKeyInfoStatus {
+ BAD = 'BAD',
+ OK = 'OK',
+ TRUSTED = 'TRUSTED',
+}
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 15b1ebf..6ccca94 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -35,7 +35,7 @@
import {GrReviewerSuggestionsProvider, SUGGESTIONS_PROVIDERS_USERS_TYPES} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
import {appContext} from '../../../services/app-context.js';
import {SpecialFilePath} from '../../../constants/constants.js';
-import {ExperimentIds} from '../../../services/flags.js';
+import {KnownExperimentId} from '../../../services/flags/flags.js';
import {fetchChangeUpdates} from '../../../utils/patch-set-util.js';
import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
import {getDisplayName} from '../../../utils/display-name-util.js';
@@ -357,7 +357,7 @@
ready() {
super.ready();
this._isPatchsetCommentsExperimentEnabled = this.flagsService
- .isEnabled(ExperimentIds.PATCHSET_COMMENTS);
+ .isEnabled(KnownExperimentId.PATCHSET_COMMENTS);
this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this);
}
@@ -556,7 +556,7 @@
}
if (this._isAttentionSetEnabled(this.serverConfig)) {
- reviewInput.ignore_default_attention_set_rules = true;
+ reviewInput.ignore_automatic_attention_set_rules = true;
reviewInput.add_to_attention_set = [];
for (const user of this._newAttentionSet) {
if (!this._currentAttentionSet.has(user)) {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
index 3a9cea7..6f24fb3 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
@@ -191,7 +191,7 @@
flushAsynchronousOperations();
stubSaveReview(review => {
- assert.isTrue(review.ignore_default_attention_set_rules);
+ assert.isTrue(review.ignore_automatic_attention_set_rules);
assert.deepEqual(review.add_to_attention_set, [{
user: 314,
reason: 'manually added in reply dialog',
diff --git a/polygerrit-ui/app/elements/gr-app-init.js b/polygerrit-ui/app/elements/gr-app-init.js
index ea10ce8..df2d58b 100644
--- a/polygerrit-ui/app/elements/gr-app-init.js
+++ b/polygerrit-ui/app/elements/gr-app-init.js
@@ -15,7 +15,7 @@
* limitations under the License.
*/
import {initAppContext} from '../services/app-context-init.js';
-import {initVisibilityReporter, initPerformanceReporter, initErrorReporter} from '../services/gr-reporting/gr-reporting.js';
+import {initVisibilityReporter, initPerformanceReporter, initErrorReporter} from '../services/gr-reporting/gr-reporting_impl.js';
import {appContext} from '../services/app-context.js';
if (!window.Polymer) {
diff --git a/polygerrit-ui/app/node_modules_licenses/licenses.ts b/polygerrit-ui/app/node_modules_licenses/licenses.ts
index 1141d6b..3315e50b 100644
--- a/polygerrit-ui/app/node_modules_licenses/licenses.ts
+++ b/polygerrit-ui/app/node_modules_licenses/licenses.ts
@@ -86,6 +86,10 @@
const packages: PackageInfo[] = [
{
+ name: "@polymer/decorators",
+ license: SharedLicenses.Polymer2017,
+ },
+ {
name: "@polymer/font-roboto",
license: SharedLicenses.Polymer2015,
},
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index 32560ff..d6e94fc 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -3,31 +3,32 @@
"description": "Gerrit Code Review - Polygerrit dependencies",
"browser": true,
"dependencies": {
+ "@polymer/decorators": "^3.0.0",
"@polymer/font-roboto-local": "^3.0.2",
"@polymer/iron-a11y-keys-behavior": "^3.0.1",
- "@polymer/iron-autogrow-textarea": "^3.0.1",
+ "@polymer/iron-autogrow-textarea": "^3.0.3",
"@polymer/iron-dropdown": "^3.0.1",
- "@polymer/iron-fit-behavior": "^3.0.1",
+ "@polymer/iron-fit-behavior": "^3.0.2",
"@polymer/iron-icon": "^3.0.1",
"@polymer/iron-iconset-svg": "^3.0.1",
"@polymer/iron-input": "^3.0.1",
- "@polymer/iron-overlay-behavior": "^3.0.2",
+ "@polymer/iron-overlay-behavior": "^3.0.3",
"@polymer/iron-selector": "^3.0.1",
"@polymer/paper-button": "^3.0.1",
"@polymer/paper-dialog": "^3.0.1",
"@polymer/paper-dialog-behavior": "^3.0.1",
"@polymer/paper-dialog-scrollable": "^3.0.1",
- "@polymer/paper-input": "^3.0.2",
+ "@polymer/paper-input": "^3.2.1",
"@polymer/paper-item": "^3.0.1",
"@polymer/paper-listbox": "^3.0.1",
"@polymer/paper-tabs": "^3.1.0",
"@polymer/paper-toggle-button": "^3.0.1",
- "@polymer/polymer": "^3.3.0",
+ "@polymer/polymer": "^3.4.1",
"@webcomponents/shadycss": "^1.9.2",
"@webcomponents/webcomponentsjs": "^1.3.3",
+ "ba-linkify": "file:../../lib/ba-linkify/src/",
"page": "^1.11.5",
"polymer-bridges": "file:../../polymer-bridges/",
- "ba-linkify": "file:../../lib/ba-linkify/src/",
"polymer-resin": "^2.0.1"
},
"license": "Apache-2.0",
diff --git a/polygerrit-ui/app/services/app-context-init.js b/polygerrit-ui/app/services/app-context-init.js
deleted file mode 100644
index 531c361..0000000
--- a/polygerrit-ui/app/services/app-context-init.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {appContext} from './app-context.js';
-import {FlagsService} from './flags.js';
-import {GrReporting} from './gr-reporting/gr-reporting.js';
-import {EventEmitter} from './gr-event-interface/gr-event-interface.js';
-import {Auth} from './gr-auth.js';
-
-const initializedServices = new Map();
-
-function getService(serviceName, serviceInit) {
- if (!initializedServices.has(serviceName)) {
- initializedServices.set(serviceName, serviceInit());
- }
- return initializedServices.get(serviceName);
-}
-
-/**
- * The AppContext lazy initializator for all services
- */
-export function initAppContext() {
- const registeredServices = {};
- function addService(serviceName, serviceCreator) {
- if (registeredServices[serviceName]) {
- throw new Error(`Service ${serviceName} already registered.`);
- }
- registeredServices[serviceName] = {
- get() {
- return getService(serviceName, serviceCreator);
- },
- };
- }
-
- addService('flagsService', () => new FlagsService());
- addService('reportingService',
- () => new GrReporting(appContext.flagsService));
- addService('eventEmitter', () => new EventEmitter());
- addService('authService', () => new Auth(appContext.eventEmitter));
- Object.defineProperties(appContext, registeredServices);
-}
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
new file mode 100644
index 0000000..b249d16
--- /dev/null
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -0,0 +1,69 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {appContext, AppContext} from './app-context';
+import {FlagsServiceImplementation} from './flags/flags_impl';
+import {GrReporting} from './gr-reporting/gr-reporting_impl';
+import {EventEmitter} from './gr-event-interface/gr-event-interface_impl';
+import {Auth} from './gr-auth/gr-auth_impl';
+
+type ServiceName = keyof AppContext;
+type ServiceCreator<T> = () => T;
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const initializedServices: Map<ServiceName, any> = new Map();
+
+function getService<K extends ServiceName>(
+ serviceName: K,
+ serviceCreator: ServiceCreator<AppContext[K]>
+): AppContext[K] {
+ if (!initializedServices.has(serviceName)) {
+ initializedServices.set(serviceName, serviceCreator());
+ }
+ return initializedServices.get(serviceName);
+}
+
+/**
+ * The AppContext lazy initializator for all services
+ */
+export function initAppContext() {
+ function populateAppContext(
+ serviceCreators: {[P in ServiceName]: ServiceCreator<AppContext[P]>}
+ ) {
+ const registeredServices = Object.keys(serviceCreators).reduce(
+ (registeredServices, key) => {
+ const serviceName = key as ServiceName;
+ const serviceCreator = serviceCreators[serviceName];
+ registeredServices[serviceName] = {
+ configurable: true, // Tests can mock properties
+ get() {
+ return getService(serviceName, serviceCreator);
+ },
+ };
+ return registeredServices;
+ },
+ {} as PropertyDescriptorMap
+ );
+ Object.defineProperties(appContext, registeredServices);
+ }
+
+ populateAppContext({
+ flagsService: () => new FlagsServiceImplementation(),
+ reportingService: () => new GrReporting(appContext.flagsService),
+ eventEmitter: () => new EventEmitter(),
+ authService: () => new Auth(appContext.eventEmitter),
+ });
+}
diff --git a/polygerrit-ui/app/services/app-context.js b/polygerrit-ui/app/services/app-context.ts
similarity index 60%
rename from polygerrit-ui/app/services/app-context.js
rename to polygerrit-ui/app/services/app-context.ts
index 3f86003..c08ee7a 100644
--- a/polygerrit-ui/app/services/app-context.js
+++ b/polygerrit-ui/app/services/app-context.ts
@@ -14,16 +14,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {FlagsService} from './flags/flags';
+import {EventEmitterService} from './gr-event-interface/gr-event-interface';
+import {ReportingService} from './gr-reporting/gr-reporting';
+import {AuthService} from './gr-auth/gr-auth';
+
+export interface AppContext {
+ flagsService: FlagsService;
+ reportingService: ReportingService;
+ eventEmitter: EventEmitterService;
+ authService: AuthService;
+}
/**
* The AppContext holds immortal singleton instances of services. It's a
* convenient way to provide singletons that can be swapped out for testing.
*
* AppContext is initialized in ./app-context-init.js
+ *
+ * It is guaranteed that all fields in appContext are always initialized
+ * (except for shared gr-diff)
*/
-export const appContext = {
- flagsService: null,
- reportingService: null,
- eventEmitter: null,
- authService: null,
-};
\ No newline at end of file
+export const appContext: AppContext = {} as AppContext;
diff --git a/polygerrit-ui/app/constants/constants.d.ts b/polygerrit-ui/app/services/flags/flags.ts
similarity index 71%
rename from polygerrit-ui/app/constants/constants.d.ts
rename to polygerrit-ui/app/services/flags/flags.ts
index 036d6ea..aae8ac7 100644
--- a/polygerrit-ui/app/constants/constants.d.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -15,9 +15,14 @@
* limitations under the License.
*/
-export type ChangeStatus = any;
-export namespace ChangeStatus {
- export const ABANDONED: string;
- export const MERGED: string;
- export const NEW: string;
+export interface FlagsService {
+ isEnabled(experimentId: string): boolean;
+ enabledExperiments: string[];
+}
+
+/**
+ * @desc Experiment ids used in Gerrit.
+ */
+export enum KnownExperimentId {
+ PATCHSET_COMMENTS = 'UiFeature__patchset_comments',
}
diff --git a/polygerrit-ui/app/services/flags.js b/polygerrit-ui/app/services/flags/flags_impl.ts
similarity index 71%
rename from polygerrit-ui/app/services/flags.js
rename to polygerrit-ui/app/services/flags/flags_impl.ts
index 6313255..835eb56 100644
--- a/polygerrit-ui/app/services/flags.js
+++ b/polygerrit-ui/app/services/flags/flags_impl.ts
@@ -14,37 +14,37 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {FlagsService} from './flags';
-/**
- * @enum
- * @desc Experiment ids used in Gerrit.
- */
-export const ExperimentIds = {
- PATCHSET_COMMENTS: 'UiFeature__patchset_comments',
-};
+declare global {
+ interface Window {
+ ENABLED_EXPERIMENTS: string[];
+ }
+}
/**
* Flags service.
*
* Provides all related methods / properties regarding on feature flags.
*/
-export class FlagsService {
+export class FlagsServiceImplementation implements FlagsService {
+ private readonly _experiments: Set<string>;
+
constructor() {
// stores all enabled experiments
- this._experiments = new Set();
- this._loadExperiments();
+ this._experiments = this._loadExperiments();
}
/**
* @param {string} experimentId
* @returns {boolean}
*/
- isEnabled(experimentId) {
+ isEnabled(experimentId: string): boolean {
return this._experiments.has(experimentId);
}
- _loadExperiments() {
- this._experiments = new Set(window.ENABLED_EXPERIMENTS);
+ _loadExperiments(): Set<string> {
+ return new Set(window.ENABLED_EXPERIMENTS);
}
/**
diff --git a/polygerrit-ui/app/services/flags_test.js b/polygerrit-ui/app/services/flags/flags_test.js
similarity index 88%
rename from polygerrit-ui/app/services/flags_test.js
rename to polygerrit-ui/app/services/flags/flags_test.js
index ae1033e..33508af 100644
--- a/polygerrit-ui/app/services/flags_test.js
+++ b/polygerrit-ui/app/services/flags/flags_test.js
@@ -15,8 +15,8 @@
* limitations under the License.
*/
-import '../test/common-test-setup-karma.js';
-import {FlagsService} from './flags.js';
+import '../../test/common-test-setup-karma.js';
+import {FlagsServiceImplementation} from './flags_impl.js';
suite('flags tests', () => {
let originalEnabledExperiments;
@@ -25,7 +25,7 @@
suiteSetup(() => {
originalEnabledExperiments = window.ENABLED_EXPERIMENTS;
window.ENABLED_EXPERIMENTS = ['a', 'a'];
- flags = new FlagsService();
+ flags = new FlagsServiceImplementation();
});
suiteTeardown(() => {
diff --git a/polygerrit-ui/app/services/gr-auth.js b/polygerrit-ui/app/services/gr-auth.js
deleted file mode 100644
index 21081cc..0000000
--- a/polygerrit-ui/app/services/gr-auth.js
+++ /dev/null
@@ -1,265 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {getBaseUrl} from '../utils/url-util.js';
-
-const MAX_AUTH_CHECK_WAIT_TIME_MS = 1000 * 30; // 30s
-const MAX_GET_TOKEN_RETRIES = 2;
-
-/**
- * Auth class.
- */
-export class Auth {
- constructor(eventEmitter) {
- this._type = null;
- this._cachedTokenPromise = null;
- this._defaultOptions = {};
- this._retriesLeft = MAX_GET_TOKEN_RETRIES;
- this._status = Auth.STATUS.UNDETERMINED;
- this._authCheckPromise = null;
- this._last_auth_check_time = Date.now();
- this.eventEmitter = eventEmitter;
- }
-
- get baseUrl() {
- return getBaseUrl();
- }
-
- /**
- * Returns if user is authed or not.
- *
- * @returns {!Promise<boolean>}
- */
- authCheck() {
- if (!this._authCheckPromise ||
- (Date.now() - this._last_auth_check_time > MAX_AUTH_CHECK_WAIT_TIME_MS)
- ) {
- // Refetch after last check expired
- this._authCheckPromise = fetch(`${this.baseUrl}/auth-check`);
- this._last_auth_check_time = Date.now();
- }
-
- return this._authCheckPromise.then(res => {
- // auth-check will return 204 if authed
- // treat the rest as unauthed
- if (res.status === 204) {
- this._setStatus(Auth.STATUS.AUTHED);
- return true;
- } else {
- this._setStatus(Auth.STATUS.NOT_AUTHED);
- return false;
- }
- }).catch(e => {
- this._setStatus(Auth.STATUS.ERROR);
- // Reset _authCheckPromise to avoid caching the failed promise
- this._authCheckPromise = null;
- return false;
- });
- }
-
- clearCache() {
- this._authCheckPromise = null;
- }
-
- /**
- * @param {Auth.STATUS} status
- */
- _setStatus(status) {
- if (this._status === status) return;
-
- if (this._status === Auth.STATUS.AUTHED) {
- this.eventEmitter.emit('auth-error', {
- message: Auth.CREDS_EXPIRED_MSG, action: 'Refresh credentials',
- });
- }
- this._status = status;
- }
-
- get status() {
- return this._status;
- }
-
- get isAuthed() {
- return this._status === Auth.STATUS.AUTHED;
- }
-
- _getToken() {
- return Promise.resolve(this._cachedTokenPromise);
- }
-
- /**
- * Enable cross-domain authentication using OAuth access token.
- *
- * @param {
- * function(): !Promise<{
- * access_token: string,
- * expires_at: number
- * }>
- * } getToken
- * @param {?{credentials:string}} defaultOptions
- */
- setup(getToken, defaultOptions) {
- this._retriesLeft = MAX_GET_TOKEN_RETRIES;
- if (getToken) {
- this._type = Auth.TYPE.ACCESS_TOKEN;
- this._cachedTokenPromise = null;
- this._getToken = getToken;
- }
- this._defaultOptions = {};
- if (defaultOptions) {
- for (const p of ['credentials']) {
- this._defaultOptions[p] = defaultOptions[p];
- }
- }
- }
-
- /**
- * Perform network fetch with authentication.
- *
- * @param {string} url
- * @param {Object=} opt_options
- * @return {!Promise<!Response>}
- */
- fetch(url, opt_options) {
- const options = {
- headers: new Headers(),
- ...this._defaultOptions,
- ...opt_options,
- };
- if (this._type === Auth.TYPE.ACCESS_TOKEN) {
- return this._getAccessToken().then(
- accessToken =>
- this._fetchWithAccessToken(url, options, accessToken)
- );
- } else {
- return this._fetchWithXsrfToken(url, options);
- }
- }
-
- _getCookie(name) {
- const key = name + '=';
- let result = '';
- document.cookie.split(';').some(c => {
- c = c.trim();
- if (c.startsWith(key)) {
- result = c.substring(key.length);
- return true;
- }
- return false;
- });
- return result;
- }
-
- _isTokenValid(token) {
- if (!token) { return false; }
- if (!token.access_token || !token.expires_at) { return false; }
-
- const expiration = new Date(parseInt(token.expires_at, 10) * 1000);
- if (Date.now() >= expiration.getTime()) { return false; }
-
- return true;
- }
-
- _fetchWithXsrfToken(url, options) {
- if (options.method && options.method !== 'GET') {
- const token = this._getCookie('XSRF_TOKEN');
- if (token) {
- options.headers.append('X-Gerrit-Auth', token);
- }
- }
- options.credentials = 'same-origin';
- return fetch(url, options);
- }
-
- /**
- * @return {!Promise<string>}
- */
- _getAccessToken() {
- if (!this._cachedTokenPromise) {
- this._cachedTokenPromise = this._getToken();
- }
- return this._cachedTokenPromise.then(token => {
- if (this._isTokenValid(token)) {
- this._retriesLeft = MAX_GET_TOKEN_RETRIES;
- return token.access_token;
- }
- if (this._retriesLeft > 0) {
- this._retriesLeft--;
- this._cachedTokenPromise = null;
- return this._getAccessToken();
- }
- // Fall back to anonymous access.
- return null;
- });
- }
-
- _fetchWithAccessToken(url, options, accessToken) {
- const params = [];
-
- if (accessToken) {
- params.push(`access_token=${accessToken}`);
- const baseUrl = this.baseUrl;
- const pathname = baseUrl ?
- url.substring(url.indexOf(baseUrl) + baseUrl.length) : url;
- if (!pathname.startsWith('/a/')) {
- url = url.replace(pathname, '/a' + pathname);
- }
- }
-
- const method = options.method || 'GET';
- let contentType = options.headers.get('Content-Type');
-
- // For all requests with body, ensure json content type.
- if (!contentType && options.body) {
- contentType = 'application/json';
- }
-
- if (method !== 'GET') {
- options.method = 'POST';
- params.push(`$m=${method}`);
- // If a request is not GET, and does not have a body, ensure text/plain
- // content type.
- if (!contentType) {
- contentType = 'text/plain';
- }
- }
-
- if (contentType) {
- options.headers.set('Content-Type', 'text/plain');
- params.push(`$ct=${encodeURIComponent(contentType)}`);
- }
-
- if (params.length) {
- url = url + (url.indexOf('?') === -1 ? '?' : '&') + params.join('&');
- }
- return fetch(url, options);
- }
-}
-
-Auth.TYPE = {
- XSRF_TOKEN: 'xsrf_token',
- ACCESS_TOKEN: 'access_token',
-};
-
-/** @enum {number} */
-Auth.STATUS = {
- UNDETERMINED: 0,
- AUTHED: 1,
- NOT_AUTHED: 2,
- ERROR: 3,
-};
-
-Auth.CREDS_EXPIRED_MSG = 'Credentials expired.';
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth.ts b/polygerrit-ui/app/services/gr-auth/gr-auth.ts
new file mode 100644
index 0000000..f7fdadf
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth.ts
@@ -0,0 +1,68 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export enum AuthType {
+ XSRF_TOKEN = 'xsrf_token',
+ ACCESS_TOKEN = 'access_token',
+}
+
+export enum AuthStatus {
+ UNDETERMINED = 0,
+ AUTHED = 1,
+ NOT_AUTHED = 2,
+ ERROR = 3,
+}
+
+export interface Token {
+ access_token?: string;
+ expires_at?: string;
+}
+
+export type GetTokenCallback = () => Promise<Token | null>;
+
+export interface DefaultAuthOptions {
+ credentials: RequestCredentials;
+}
+
+export interface AuthRequestInit extends RequestInit {
+ // RequestInit define headers as HeadersInit, i.e.
+ // Headers | string[][] | Record<string, string>
+ // Auth class supports only Headers in options
+ headers?: Headers;
+}
+
+export interface AuthService {
+ baseUrl: string;
+ isAuthed: boolean;
+
+ /**
+ * Returns if user is authed or not.
+ */
+ authCheck(): Promise<boolean>;
+
+ clearCache(): void;
+
+ /**
+ * Enable cross-domain authentication using OAuth access token.
+ */
+ setup(getToken: GetTokenCallback, defaultOptions: DefaultAuthOptions): void;
+
+ /**
+ * Perform network fetch with authentication.
+ */
+ fetch(url: string, opt_options?: AuthRequestInit): Promise<Response>;
+}
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
new file mode 100644
index 0000000..b5330e7
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
@@ -0,0 +1,291 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {getBaseUrl} from '../../utils/url-util';
+import {EventEmitterService} from '../gr-event-interface/gr-event-interface';
+import {
+ AuthRequestInit,
+ AuthService,
+ AuthStatus,
+ AuthType,
+ DefaultAuthOptions,
+ GetTokenCallback,
+ Token,
+} from './gr-auth';
+
+const MAX_AUTH_CHECK_WAIT_TIME_MS = 1000 * 30; // 30s
+const MAX_GET_TOKEN_RETRIES = 2;
+
+interface ValidToken extends Token {
+ access_token: string;
+ expires_at: string;
+}
+
+interface AuthRequestInitWithHeaders extends AuthRequestInit {
+ // RequestInit define headers as optional property with a type
+ // Headers | string[][] | Record<string, string>
+ // In Auth class headers property is always set and has type Headers
+ headers: Headers;
+}
+
+/**
+ * Auth class.
+ */
+export class Auth implements AuthService {
+ // TODO(dmfilippov): Remove Type and Status properties, expose AuthType and
+ // AuthStatus to API
+ static TYPE = {
+ XSRF_TOKEN: AuthType.XSRF_TOKEN,
+ ACCESS_TOKEN: AuthType.ACCESS_TOKEN,
+ };
+
+ static STATUS = {
+ UNDETERMINED: AuthStatus.UNDETERMINED,
+ AUTHED: AuthStatus.AUTHED,
+ NOT_AUTHED: AuthStatus.NOT_AUTHED,
+ ERROR: AuthStatus.ERROR,
+ };
+
+ static CREDS_EXPIRED_MSG = 'Credentials expired.';
+
+ private _authCheckPromise?: Promise<Response>;
+
+ private _last_auth_check_time: number = Date.now();
+
+ private _status = AuthStatus.UNDETERMINED;
+
+ private _retriesLeft = MAX_GET_TOKEN_RETRIES;
+
+ private _cachedTokenPromise: Promise<Token | null> | null = null;
+
+ private _type?: AuthType;
+
+ private _defaultOptions: AuthRequestInit = {};
+
+ private _getToken: GetTokenCallback;
+
+ public eventEmitter: EventEmitterService;
+
+ constructor(eventEmitter: EventEmitterService) {
+ this._getToken = () => Promise.resolve(this._cachedTokenPromise);
+ this.eventEmitter = eventEmitter;
+ }
+
+ get baseUrl() {
+ return getBaseUrl();
+ }
+
+ /**
+ * Returns if user is authed or not.
+ */
+ authCheck(): Promise<boolean> {
+ if (
+ !this._authCheckPromise ||
+ Date.now() - this._last_auth_check_time > MAX_AUTH_CHECK_WAIT_TIME_MS
+ ) {
+ // Refetch after last check expired
+ this._authCheckPromise = fetch(`${this.baseUrl}/auth-check`);
+ this._last_auth_check_time = Date.now();
+ }
+
+ return this._authCheckPromise
+ .then(res => {
+ // auth-check will return 204 if authed
+ // treat the rest as unauthed
+ if (res.status === 204) {
+ this._setStatus(Auth.STATUS.AUTHED);
+ return true;
+ } else {
+ this._setStatus(Auth.STATUS.NOT_AUTHED);
+ return false;
+ }
+ })
+ .catch(() => {
+ this._setStatus(AuthStatus.ERROR);
+ // Reset _authCheckPromise to avoid caching the failed promise
+ this._authCheckPromise = undefined;
+ return false;
+ });
+ }
+
+ clearCache() {
+ this._authCheckPromise = undefined;
+ }
+
+ private _setStatus(status: AuthStatus) {
+ if (this._status === status) return;
+
+ if (this._status === AuthStatus.AUTHED) {
+ this.eventEmitter.emit('auth-error', {
+ message: Auth.CREDS_EXPIRED_MSG,
+ action: 'Refresh credentials',
+ });
+ }
+ this._status = status;
+ }
+
+ get status() {
+ return this._status;
+ }
+
+ get isAuthed() {
+ return this._status === Auth.STATUS.AUTHED;
+ }
+
+ /**
+ * Enable cross-domain authentication using OAuth access token.
+ */
+ setup(getToken: GetTokenCallback, defaultOptions: DefaultAuthOptions) {
+ this._retriesLeft = MAX_GET_TOKEN_RETRIES;
+ if (getToken) {
+ this._type = AuthType.ACCESS_TOKEN;
+ this._cachedTokenPromise = null;
+ this._getToken = getToken;
+ }
+ this._defaultOptions = {};
+ if (defaultOptions) {
+ this._defaultOptions.credentials = defaultOptions.credentials;
+ }
+ }
+
+ /**
+ * Perform network fetch with authentication.
+ */
+ fetch(url: string, opt_options?: AuthRequestInit): Promise<Response> {
+ const options: AuthRequestInitWithHeaders = {
+ headers: new Headers(),
+ ...this._defaultOptions,
+ ...opt_options,
+ };
+ if (this._type === AuthType.ACCESS_TOKEN) {
+ return this._getAccessToken().then(accessToken =>
+ this._fetchWithAccessToken(url, options, accessToken)
+ );
+ } else {
+ return this._fetchWithXsrfToken(url, options);
+ }
+ }
+
+ private _getCookie(name: string): string {
+ const key = name + '=';
+ let result = '';
+ document.cookie.split(';').some(c => {
+ c = c.trim();
+ if (c.startsWith(key)) {
+ result = c.substring(key.length);
+ return true;
+ }
+ return false;
+ });
+ return result;
+ }
+
+ private _isTokenValid(token: Token | null): token is ValidToken {
+ if (!token) {
+ return false;
+ }
+ if (!token.access_token || !token.expires_at) {
+ return false;
+ }
+
+ const expiration = new Date(parseInt(token.expires_at, 10) * 1000);
+ if (Date.now() >= expiration.getTime()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private _fetchWithXsrfToken(
+ url: string,
+ options: AuthRequestInitWithHeaders
+ ): Promise<Response> {
+ if (options.method && options.method !== 'GET') {
+ const token = this._getCookie('XSRF_TOKEN');
+ if (token) {
+ options.headers.append('X-Gerrit-Auth', token);
+ }
+ }
+ options.credentials = 'same-origin';
+ return fetch(url, options);
+ }
+
+ private _getAccessToken(): Promise<string | null> {
+ if (!this._cachedTokenPromise) {
+ this._cachedTokenPromise = this._getToken();
+ }
+ return this._cachedTokenPromise.then(token => {
+ if (this._isTokenValid(token)) {
+ this._retriesLeft = MAX_GET_TOKEN_RETRIES;
+ return token.access_token;
+ }
+ if (this._retriesLeft > 0) {
+ this._retriesLeft--;
+ this._cachedTokenPromise = null;
+ return this._getAccessToken();
+ }
+ // Fall back to anonymous access.
+ return null;
+ });
+ }
+
+ private _fetchWithAccessToken(
+ url: string,
+ options: AuthRequestInitWithHeaders,
+ accessToken: string | null
+ ): Promise<Response> {
+ const params = [];
+
+ if (accessToken) {
+ params.push(`access_token=${accessToken}`);
+ const baseUrl = this.baseUrl;
+ const pathname = baseUrl
+ ? url.substring(url.indexOf(baseUrl) + baseUrl.length)
+ : url;
+ if (!pathname.startsWith('/a/')) {
+ url = url.replace(pathname, '/a' + pathname);
+ }
+ }
+
+ const method = options.method || 'GET';
+ let contentType = options.headers.get('Content-Type');
+
+ // For all requests with body, ensure json content type.
+ if (!contentType && options.body) {
+ contentType = 'application/json';
+ }
+
+ if (method !== 'GET') {
+ options.method = 'POST';
+ params.push(`$m=${method}`);
+ // If a request is not GET, and does not have a body, ensure text/plain
+ // content type.
+ if (!contentType) {
+ contentType = 'text/plain';
+ }
+ }
+
+ if (contentType) {
+ options.headers.set('Content-Type', 'text/plain');
+ params.push(`$ct=${encodeURIComponent(contentType)}`);
+ }
+
+ if (params.length) {
+ url = url + (url.indexOf('?') === -1 ? '?' : '&') + params.join('&');
+ }
+ return fetch(url, options);
+ }
+}
diff --git a/polygerrit-ui/app/services/gr-auth_test.js b/polygerrit-ui/app/services/gr-auth/gr-auth_test.js
similarity index 98%
rename from polygerrit-ui/app/services/gr-auth_test.js
rename to polygerrit-ui/app/services/gr-auth/gr-auth_test.js
index 541cd42..432518c 100644
--- a/polygerrit-ui/app/services/gr-auth_test.js
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_test.js
@@ -15,10 +15,10 @@
* limitations under the License.
*/
-import '../test/common-test-setup-karma.js';
-import {Auth} from './gr-auth.js';
-import {appContext} from './app-context.js';
-import {stubBaseUrl} from '../test/test-utils.js';
+import '../../test/common-test-setup-karma.js';
+import {Auth} from './gr-auth_impl.js';
+import {appContext} from '../app-context.js';
+import {stubBaseUrl} from '../../test/test-utils.js';
suite('gr-auth', () => {
let auth;
diff --git a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface.ts b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface.ts
new file mode 100644
index 0000000..d59a022
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface.ts
@@ -0,0 +1,67 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export type EventCallback = (...args: any) => void;
+export type UnsubscribeMethod = () => void;
+
+export interface EventEmitterService {
+ /**
+ * Register an event listener to an event.
+ */
+ addListener(eventName: string, cb: EventCallback): UnsubscribeMethod;
+
+ /**
+ * Alias for addListener.
+ */
+ on(eventName: string, cb: EventCallback): UnsubscribeMethod;
+
+ /**
+ * Attach event handler only once. Automatically removed.
+ */
+ once(eventName: string, cb: EventCallback): UnsubscribeMethod;
+
+ /**
+ * De-register an event listener to an event.
+ */
+ removeListener(eventName: string, cb: EventCallback): void;
+
+ /**
+ * Alias to removeListener
+ */
+ off(eventName: string, cb: EventCallback): void;
+
+ /**
+ * Synchronously calls each of the listeners registered for
+ * the event named eventName, in the order they were registered,
+ * passing the supplied detail to each.
+ *
+ * @returns true if the event had listeners, false otherwise.
+ */
+ emit(eventName: string, detail: any): boolean;
+
+ /**
+ * Alias to emit.
+ */
+ dispatch(eventName: string, detail: any): boolean;
+
+ /**
+ * Remove listeners for a specific event or all.
+ *
+ * @param eventName if not provided, will remove all
+ */
+ removeAllListeners(eventName: string): void;
+}
diff --git a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface.js b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_impl.ts
similarity index 69%
rename from polygerrit-ui/app/services/gr-event-interface/gr-event-interface.js
rename to polygerrit-ui/app/services/gr-event-interface/gr-event-interface_impl.ts
index 7705874..72afbda 100644
--- a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface.js
+++ b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_impl.ts
@@ -15,6 +15,11 @@
* limitations under the License.
*/
+import {
+ EventCallback,
+ EventEmitterService,
+ UnsubscribeMethod,
+} from './gr-event-interface';
/**
* An lite implementation of
* https://nodejs.org/api/events.html#events_class_eventemitter.
@@ -29,27 +34,16 @@
* }
*
*/
-export class EventEmitter {
- constructor() {
- /**
- * Shared events map from name to the listeners.
- *
- * @type {!Object<string, Array<eventCallback>>}
- */
- this._listenersMap = new Map();
- }
+export class EventEmitter implements EventEmitterService {
+ private _listenersMap = new Map<string, EventCallback[]>();
/**
* Register an event listener to an event.
- *
- * @param {string} eventName
- * @param {eventCallback} cb
- * @returns {Function} Unsubscribe method
*/
- addListener(eventName, cb) {
+ addListener(eventName: string, cb: EventCallback): UnsubscribeMethod {
if (!eventName || !cb) {
console.warn('A valid eventname and callback is required!');
- return;
+ return () => {};
}
const listeners = this._listenersMap.get(eventName) || [];
@@ -61,14 +55,18 @@
};
}
- // Alias for addListener.
- on(eventName, cb) {
+ /**
+ * Alias for addListener.
+ */
+ on(eventName: string, cb: EventCallback): UnsubscribeMethod {
return this.addListener(eventName, cb);
}
- // Attach event handler only once. Automatically removed.
- once(eventName, cb) {
- const onceWrapper = (...args) => {
+ /**
+ * Attach event handler only once. Automatically removed.
+ */
+ once(eventName: string, cb: EventCallback): UnsubscribeMethod {
+ const onceWrapper = (...args: any[]) => {
cb(...args);
this.off(eventName, onceWrapper);
};
@@ -77,18 +75,17 @@
/**
* De-register an event listener to an event.
- *
- * @param {string} eventName
- * @param {eventCallback} cb
*/
- removeListener(eventName, cb) {
+ removeListener(eventName: string, cb: EventCallback): void {
let listeners = this._listenersMap.get(eventName) || [];
listeners = listeners.filter(listener => listener !== cb);
this._listenersMap.set(eventName, listeners);
}
- // Alias to removeListener
- off(eventName, cb) {
+ /**
+ * Alias to removeListener
+ */
+ off(eventName: string, cb: EventCallback): void {
this.removeListener(eventName, cb);
}
@@ -97,12 +94,9 @@
* the event named eventName, in the order they were registered,
* passing the supplied detail to each.
*
- * Returns true if the event had listeners, false otherwise.
- *
- * @param {string} eventName
- * @param {*} detail
+ * @returns true if the event had listeners, false otherwise.
*/
- emit(eventName, detail) {
+ emit(eventName: string, detail: any): boolean {
const listeners = this._listenersMap.get(eventName) || [];
for (const listener of listeners) {
try {
@@ -114,17 +108,19 @@
return listeners.length !== 0;
}
- // Alias to emit.
- dispatch(eventName, detail) {
+ /**
+ * Alias to emit.
+ */
+ dispatch(eventName: string, detail: any): boolean {
return this.emit(eventName, detail);
}
/**
* Remove listeners for a specific event or all.
*
- * @param {string} eventName if not provided, will remove all
+ * @param eventName if not provided, will remove all
*/
- removeAllListeners(eventName) {
+ removeAllListeners(eventName: string): void {
if (eventName) {
this._listenersMap.set(eventName, []);
} else {
diff --git a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js
index 1cdd6e3..6d0ab7b 100644
--- a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js
+++ b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js
@@ -17,7 +17,7 @@
import '../../test/common-test-setup-karma.js';
import '../../elements/shared/gr-js-api-interface/gr-js-api-interface.js';
-import {EventEmitter} from './gr-event-interface.js';
+import {EventEmitter} from './gr-event-interface_impl.js';
import {_testOnly_initGerritPluginApi} from '../../elements/shared/gr-js-api-interface/gr-gerrit.js';
const basicFixture = fixtureFromElement('gr-js-api-interface');
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting.js b/polygerrit-ui/app/services/gr-reporting/gr-reporting.js
deleted file mode 100644
index ea69d5f..0000000
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting.js
+++ /dev/null
@@ -1,654 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Latency reporting constants.
-
-const TIMING = {
- TYPE: 'timing-report',
- CATEGORY: {
- UI_LATENCY: 'UI Latency',
- RPC: 'RPC Timing',
- },
- EVENT: {
- APP_STARTED: 'App Started',
- },
-};
-
-const LIFECYCLE = {
- TYPE: 'lifecycle',
- CATEGORY: {
- DEFAULT: 'Default',
- EXTENSION_DETECTED: 'Extension detected',
- PLUGINS_INSTALLED: 'Plugins installed',
- },
-};
-
-const INTERACTION = {
- TYPE: 'interaction',
- CATEGORY: {
- DEFAULT: 'Default',
- VISIBILITY: 'Visibility',
- },
-};
-
-const NAVIGATION = {
- TYPE: 'nav-report',
- CATEGORY: {
- LOCATION_CHANGED: 'Location Changed',
- },
- EVENT: {
- PAGE: 'Page',
- },
-};
-
-const ERROR = {
- TYPE: 'error',
- CATEGORY: {
- EXCEPTION: 'exception',
- ERROR_DIALOG: 'Error Dialog',
- },
-};
-
-const TIMER = {
- CHANGE_DISPLAYED: 'ChangeDisplayed',
- CHANGE_LOAD_FULL: 'ChangeFullyLoaded',
- DASHBOARD_DISPLAYED: 'DashboardDisplayed',
- DIFF_VIEW_CONTENT_DISPLAYED: 'DiffViewOnlyContent',
- DIFF_VIEW_DISPLAYED: 'DiffViewDisplayed',
- DIFF_VIEW_LOAD_FULL: 'DiffViewFullyLoaded',
- FILE_LIST_DISPLAYED: 'FileListDisplayed',
- PLUGINS_LOADED: 'PluginsLoaded',
- STARTUP_CHANGE_DISPLAYED: 'StartupChangeDisplayed',
- STARTUP_CHANGE_LOAD_FULL: 'StartupChangeFullyLoaded',
- STARTUP_DASHBOARD_DISPLAYED: 'StartupDashboardDisplayed',
- STARTUP_DIFF_VIEW_CONTENT_DISPLAYED: 'StartupDiffViewOnlyContent',
- STARTUP_DIFF_VIEW_DISPLAYED: 'StartupDiffViewDisplayed',
- STARTUP_DIFF_VIEW_LOAD_FULL: 'StartupDiffViewFullyLoaded',
- STARTUP_FILE_LIST_DISPLAYED: 'StartupFileListDisplayed',
- WEB_COMPONENTS_READY: 'WebComponentsReady',
- METRICS_PLUGIN_LOADED: 'MetricsPluginLoaded',
-};
-
-const STARTUP_TIMERS = {};
-STARTUP_TIMERS[TIMER.PLUGINS_LOADED] = 0;
-STARTUP_TIMERS[TIMER.METRICS_PLUGIN_LOADED] = 0;
-STARTUP_TIMERS[TIMER.STARTUP_CHANGE_DISPLAYED] = 0;
-STARTUP_TIMERS[TIMER.STARTUP_CHANGE_LOAD_FULL] = 0;
-STARTUP_TIMERS[TIMER.STARTUP_DASHBOARD_DISPLAYED] = 0;
-STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED] = 0;
-STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_DISPLAYED] = 0;
-STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_LOAD_FULL] = 0;
-STARTUP_TIMERS[TIMER.STARTUP_FILE_LIST_DISPLAYED] = 0;
-STARTUP_TIMERS[TIMING.EVENT.APP_STARTED] = 0;
-// WebComponentsReady timer is triggered from gr-router.
-STARTUP_TIMERS[TIMER.WEB_COMPONENTS_READY] = 0;
-
-const DRAFT_ACTION_TIMER = 'TimeBetweenDraftActions';
-const DRAFT_ACTION_TIMER_MAX = 2 * 60 * 1000; // 2 minutes.
-const SLOW_RPC_THRESHOLD = 500;
-
-export function initErrorReporter(appContext) {
- const reportingService = appContext.reportingService;
- const onError = function(oldOnError, msg, url, line, column, error) {
- if (oldOnError) {
- oldOnError(msg, url, line, column, error);
- }
- if (error) {
- line = line || error.lineNumber;
- column = column || error.columnNumber;
- let shortenedErrorStack = msg;
- if (error.stack) {
- const errorStackLines = error.stack.split('\n');
- shortenedErrorStack = errorStackLines.slice(0,
- Math.min(3, errorStackLines.length)).join('\n');
- }
- msg = shortenedErrorStack || error.toString();
- }
- const payload = {
- url,
- line,
- column,
- error,
- };
- reportingService.reporter(ERROR.TYPE, ERROR.CATEGORY.EXCEPTION,
- msg, payload);
- return true;
- };
-
- const catchErrors = function(opt_context) {
- const context = opt_context || window;
- context.onerror = onError.bind(null, context.onerror);
- context.addEventListener('unhandledrejection', e => {
- const msg = e.reason.message;
- const payload = {
- error: e.reason,
- };
- reportingService.reporter(ERROR.TYPE,
- ERROR.CATEGORY.EXCEPTION, msg, payload);
- });
- };
-
- catchErrors();
-
- // for testing
- return {catchErrors};
-}
-
-export function initPerformanceReporter(appContext) {
- const reportingService = appContext.reportingService;
- // PerformanceObserver interface is a browser API.
- if (window.PerformanceObserver) {
- const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || [];
- // Safari doesn't support longtask yet
- if (supportedEntryTypes.includes('longtask')) {
- const catchLongJsTasks = new PerformanceObserver(list => {
- for (const task of list.getEntries()) {
- // We are interested in longtask longer than 200 ms (default is 50 ms)
- if (task.duration > 200) {
- reportingService.reporter(TIMING.TYPE,
- TIMING.CATEGORY.UI_LATENCY, `Task ${task.name}`,
- Math.round(task.duration), {}, false);
- }
- }
- });
- catchLongJsTasks.observe({entryTypes: ['longtask']});
- }
- }
-}
-
-export function initVisibilityReporter(appContext) {
- const reportingService = appContext.reportingService;
- document.addEventListener('visibilitychange', () => {
- reportingService.onVisibilityChange();
- });
-}
-
-// Calculates the time of Gerrit being in a background tab. When Gerrit reports
-// a pageLoad metric it’s attached to its details for latency analysis.
-// It resets on locationChange.
-class HiddenDurationTimer {
- constructor() {
- this.reset();
- }
-
- reset() {
- this.accHiddenDurationMs = 0;
- this.lastVisibleTimestampMs = 0;
- }
-
- onVisibilityChange() {
- if (document.visibilityState === 'hidden') {
- this.lastVisibleTimestampMs = now();
- } else if (document.visibilityState === 'visible') {
- if (this.lastVisibleTimestampMs !== null) {
- this.accHiddenDurationMs += now() - this.lastVisibleTimestampMs;
- // Set to null for guarding against two 'visible' events in a row.
- this.lastVisibleTimestampMs = null;
- }
- }
- }
-
- get hiddenDurationMs() {
- if (document.visibilityState === 'hidden'
- && this.lastVisibleTimestampMs !== null) {
- return this.accHiddenDurationMs + now() - this.lastVisibleTimestampMs;
- }
- return this.accHiddenDurationMs;
- }
-}
-
-export function now() {
- return Math.round(window.performance.now());
-}
-
-export class GrReporting {
- constructor(flagsService) {
- this._flagsService = flagsService;
- this._baselines = STARTUP_TIMERS;
- this._timers = {
- timeBetweenDraftActions: null,
- };
- this._reportRepoName = undefined;
- this._pending = [];
- this._slowRpcList = [];
- this.hiddenDurationTimer = new HiddenDurationTimer();
- }
-
- get performanceTiming() {
- return window.performance.timing;
- }
-
- get slowRpcSnapshot() {
- return (this._slowRpcList || []).slice();
- }
-
- _arePluginsLoaded() {
- return this._baselines &&
- !this._baselines.hasOwnProperty(TIMER.PLUGINS_LOADED);
- }
-
- _isMetricsPluginLoaded() {
- return this._arePluginsLoaded() || this._baselines &&
- !this._baselines.hasOwnProperty(TIMER.METRICS_PLUGIN_LOADED);
- }
-
- /**
- * Reporter reports events. Events will be queued if metrics plugin is not
- * yet installed.
- *
- * @param {string} type
- * @param {string} category
- * @param {string} eventName
- * @param {string|number} eventValue
- * @param {Object} eventDetails
- * @param {boolean|undefined} opt_noLog If true, the event will not be
- * logged to the JS console.
- */
- reporter(type, category, eventName, eventValue, eventDetails, opt_noLog) {
- const eventInfo = this._createEventInfo(type, category,
- eventName, eventValue, eventDetails);
- if (type === ERROR.TYPE && category === ERROR.CATEGORY.EXCEPTION) {
- console.error(eventValue && eventValue.error || eventName);
- }
-
- // We report events immediately when metrics plugin is loaded
- if (this._isMetricsPluginLoaded() && !this._pending.length) {
- this._reportEvent(eventInfo, opt_noLog);
- } else {
- // We cache until metrics plugin is loaded
- this._pending.push([eventInfo, opt_noLog]);
- if (this._isMetricsPluginLoaded()) {
- this._pending.forEach(([eventInfo, opt_noLog]) => {
- this._reportEvent(eventInfo, opt_noLog);
- });
- this._pending = [];
- }
- }
- }
-
- _reportEvent(eventInfo, opt_noLog) {
- const {type, value, name} = eventInfo;
- document.dispatchEvent(new CustomEvent(type, {detail: eventInfo}));
- if (opt_noLog) { return; }
- if (type !== ERROR.TYPE) {
- if (value !== undefined) {
- console.info(`Reporting: ${name}: ${value}`);
- } else {
- console.info(`Reporting: ${name}`);
- }
- }
- }
-
- _createEventInfo(type, category, name, value, eventDetails) {
- const eventInfo = {
- type,
- category,
- name,
- value,
- eventStart: now(),
- };
-
- if (typeof(eventDetails) === 'object' &&
- Object.entries(eventDetails).length !== 0) {
- eventInfo.eventDetails = JSON.stringify(eventDetails);
- }
-
- if (this._reportRepoName) {
- eventInfo.repoName = this._reportRepoName;
- }
-
- const isInBackgroundTab = document.visibilityState === 'hidden';
- if (isInBackgroundTab !== undefined) {
- eventInfo.inBackgroundTab = isInBackgroundTab;
- }
-
- if (this._flagsService.enabledExperiments.length) {
- eventInfo.enabledExperiments =
- JSON.stringify(this._flagsService.enabledExperiments);
- }
-
- return eventInfo;
- }
-
- /**
- * User-perceived app start time, should be reported when the app is ready.
- */
- appStarted() {
- this.timeEnd(TIMING.EVENT.APP_STARTED);
- this._reportNavResTimes();
- }
-
- onVisibilityChange() {
- this.hiddenDurationTimer.onVisibilityChange();
- const eventName = `Visibility changed to ${document.visibilityState}`;
- this.reporter(LIFECYCLE.TYPE, LIFECYCLE.CATEGORY.VISIBILITY,
- eventName, undefined, {
- hiddenDurationMs: this.hiddenDurationTimer.hiddenDurationMs,
- }, true);
- }
-
- /**
- * Browser's navigation and resource timings
- */
- _reportNavResTimes() {
- const perfEvents = Object.keys(this.performanceTiming.toJSON());
- perfEvents.forEach(
- eventName => this._reportPerformanceTiming(eventName)
- );
- }
-
- _reportPerformanceTiming(eventName, eventDetails) {
- const eventTiming = this.performanceTiming[eventName];
- if (eventTiming > 0) {
- const elapsedTime = eventTiming -
- this.performanceTiming.navigationStart;
- // NavResTime - Navigation and resource timings.
- this.reporter(TIMING.TYPE, TIMING.CATEGORY.UI_LATENCY,
- `NavResTime - ${eventName}`, elapsedTime, eventDetails, true);
- }
- }
-
- beforeLocationChanged() {
- for (const prop of Object.keys(this._baselines)) {
- delete this._baselines[prop];
- }
- this.time(TIMER.CHANGE_DISPLAYED);
- this.time(TIMER.CHANGE_LOAD_FULL);
- this.time(TIMER.DASHBOARD_DISPLAYED);
- this.time(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
- this.time(TIMER.DIFF_VIEW_DISPLAYED);
- this.time(TIMER.DIFF_VIEW_LOAD_FULL);
- this.time(TIMER.FILE_LIST_DISPLAYED);
- this._reportRepoName = undefined;
- // reset slow rpc list since here start page loads which report these rpcs
- this._slowRpcList = [];
- this.hiddenDurationTimer.reset();
- }
-
- locationChanged(page) {
- this.reporter(NAVIGATION.TYPE, NAVIGATION.CATEGORY.LOCATION_CHANGED,
- NAVIGATION.EVENT.PAGE, page);
- }
-
- dashboardDisplayed() {
- if (this._baselines.hasOwnProperty(TIMER.STARTUP_DASHBOARD_DISPLAYED)) {
- this.timeEnd(TIMER.STARTUP_DASHBOARD_DISPLAYED, this._pageLoadDetails());
- } else {
- this.timeEnd(TIMER.DASHBOARD_DISPLAYED, this._pageLoadDetails());
- }
- }
-
- changeDisplayed() {
- if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_DISPLAYED)) {
- this.timeEnd(TIMER.STARTUP_CHANGE_DISPLAYED, this._pageLoadDetails());
- } else {
- this.timeEnd(TIMER.CHANGE_DISPLAYED, this._pageLoadDetails());
- }
- }
-
- changeFullyLoaded() {
- if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_LOAD_FULL)) {
- this.timeEnd(TIMER.STARTUP_CHANGE_LOAD_FULL);
- } else {
- this.timeEnd(TIMER.CHANGE_LOAD_FULL);
- }
- }
-
- diffViewDisplayed() {
- if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_DISPLAYED)) {
- this.timeEnd(TIMER.STARTUP_DIFF_VIEW_DISPLAYED, this._pageLoadDetails());
- } else {
- this.timeEnd(TIMER.DIFF_VIEW_DISPLAYED, this._pageLoadDetails());
- }
- }
-
- diffViewFullyLoaded() {
- if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL)) {
- this.timeEnd(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL);
- } else {
- this.timeEnd(TIMER.DIFF_VIEW_LOAD_FULL);
- }
- }
-
- diffViewContentDisplayed() {
- if (this._baselines.hasOwnProperty(
- TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED)) {
- this.timeEnd(TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED);
- } else {
- this.timeEnd(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
- }
- }
-
- fileListDisplayed() {
- if (this._baselines.hasOwnProperty(TIMER.STARTUP_FILE_LIST_DISPLAYED)) {
- this.timeEnd(TIMER.STARTUP_FILE_LIST_DISPLAYED);
- } else {
- this.timeEnd(TIMER.FILE_LIST_DISPLAYED);
- }
- }
-
- _pageLoadDetails() {
- const details = {
- rpcList: this.slowRpcSnapshot,
- };
-
- if (window.screen) {
- details.screenSize = {
- width: window.screen.width,
- height: window.screen.height,
- };
- }
-
- if (document && document.documentElement) {
- details.viewport = {
- width: document.documentElement.clientWidth,
- height: document.documentElement.clientHeight,
- };
- }
-
- if (window.performance && window.performance.memory) {
- const toMb = bytes => Math.round((bytes / (1024 * 1024)) * 100) / 100;
- details.usedJSHeapSizeMb =
- toMb(window.performance.memory.usedJSHeapSize);
- }
-
- details.hiddenDurationMs = this.hiddenDurationTimer.hiddenDurationMs;
- return details;
- }
-
- reportExtension(name) {
- this.reporter(LIFECYCLE.TYPE, LIFECYCLE.CATEGORY.EXTENSION_DETECTED, name);
- }
-
- pluginLoaded(name) {
- if (name.startsWith('metrics-')) {
- this.timeEnd(TIMER.METRICS_PLUGIN_LOADED);
- }
- }
-
- pluginsLoaded(pluginsList) {
- this.timeEnd(TIMER.PLUGINS_LOADED);
- this.reporter(
- LIFECYCLE.TYPE, LIFECYCLE.CATEGORY.PLUGINS_INSTALLED,
- LIFECYCLE.CATEGORY.PLUGINS_INSTALLED, undefined,
- {pluginsList: pluginsList || []}, true);
- }
-
- /**
- * Reset named timer.
- */
- time(name) {
- this._baselines[name] = now();
- window.performance.mark(`${name}-start`);
- }
-
- /**
- * Finish named timer and report it to server.
- */
- timeEnd(name, eventDetails) {
- if (!this._baselines.hasOwnProperty(name)) { return; }
- const baseTime = this._baselines[name];
- delete this._baselines[name];
- this._reportTiming(name, now() - baseTime, eventDetails);
-
- // Finalize the interval. Either from a registered start mark or
- // the navigation start time (if baseTime is 0).
- if (baseTime !== 0) {
- window.performance.measure(name, `${name}-start`);
- } else {
- // Microsft Edge does not handle the 2nd param correctly
- // (if undefined).
- window.performance.measure(name);
- }
- }
-
- /**
- * Reports just line timeEnd, but additionally reports an average given a
- * denominator and a separate reporiting name for the average.
- *
- * @param {string} name Timing name.
- * @param {string} averageName Average timing name.
- * @param {number} denominator Number by which to divide the total to
- * compute the average.
- */
- timeEndWithAverage(name, averageName, denominator) {
- if (!this._baselines.hasOwnProperty(name)) { return; }
- const baseTime = this._baselines[name];
- this.timeEnd(name);
-
- // Guard against division by zero.
- if (!denominator) { return; }
- const time = now() - baseTime;
- this._reportTiming(averageName, time / denominator);
- }
-
- /**
- * Send a timing report with an arbitrary time value.
- *
- * @param {string} name Timing name.
- * @param {number} time The time to report as an integer of milliseconds.
- * @param {Object} eventDetails non sensitive details
- */
- _reportTiming(name, time, eventDetails) {
- this.reporter(TIMING.TYPE, TIMING.CATEGORY.UI_LATENCY, name, time,
- eventDetails);
- }
-
- /**
- * Get a timer object to for reporing a user timing. The start time will be
- * the time that the object has been created, and the end time will be the
- * time that the "end" method is called on the object.
- *
- * @param {string} name Timing name.
- * @returns {!Object} The timer object.
- */
- getTimer(name) {
- let called = false;
- let start;
- let max = null;
-
- const timer = {
-
- // Clear the timer and reset the start time.
- reset: () => {
- called = false;
- start = now();
- return timer;
- },
-
- // Stop the timer and report the intervening time.
- end: () => {
- if (called) {
- throw new Error(`Timer for "${name}" already ended.`);
- }
- called = true;
- const time = now() - start;
-
- // If a maximum is specified and the time exceeds it, do not report.
- if (max && time > max) { return timer; }
-
- this._reportTiming(name, time);
- return timer;
- },
-
- // Set a maximum reportable time. If a maximum is set and the timer is
- // ended after the specified amount of time, the value is not reported.
- withMaximum(maximum) {
- max = maximum;
- return timer;
- },
- };
-
- // The timer is initialized to its creation time.
- return timer.reset();
- }
-
- /**
- * Log timing information for an RPC.
- *
- * @param {string} anonymizedUrl The URL of the RPC with tokens obfuscated.
- * @param {number} elapsed The time elapsed of the RPC.
- */
- reportRpcTiming(anonymizedUrl, elapsed) {
- this.reporter(TIMING.TYPE, TIMING.CATEGORY.RPC, 'RPC-' + anonymizedUrl,
- elapsed, {}, true);
- if (elapsed >= SLOW_RPC_THRESHOLD) {
- this._slowRpcList.push({anonymizedUrl, elapsed});
- }
- }
-
- reportLifeCycle(eventName, details) {
- this.reporter(LIFECYCLE.TYPE, LIFECYCLE.CATEGORY.DEFAULT, eventName,
- undefined, details, true);
- }
-
- reportInteraction(eventName, details) {
- this.reporter(INTERACTION.TYPE, INTERACTION.CATEGORY.DEFAULT, eventName,
- undefined, details, true);
- }
-
- /**
- * A draft interaction was started. Update the time-betweeen-draft-actions
- * timer.
- */
- recordDraftInteraction() {
- // If there is no timer defined, then this is the first interaction.
- // Set up the timer so that it's ready to record the intervening time when
- // called again.
- const timer = this._timers.timeBetweenDraftActions;
- if (!timer) {
- // Create a timer with a maximum length.
- this._timers.timeBetweenDraftActions = this.getTimer(DRAFT_ACTION_TIMER)
- .withMaximum(DRAFT_ACTION_TIMER_MAX);
- return;
- }
-
- // Mark the time and reinitialize the timer.
- timer.end().reset();
- }
-
- reportErrorDialog(message) {
- this.reporter(ERROR.TYPE, ERROR.CATEGORY.ERROR_DIALOG,
- 'ErrorDialog: ' + message, {error: new Error(message)});
- }
-
- setRepoName(repoName) {
- this._reportRepoName = repoName;
- }
-}
-
-export const DEFAULT_STARTUP_TIMERS = {...STARTUP_TIMERS};
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
new file mode 100644
index 0000000..8c70df3
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
@@ -0,0 +1,97 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export type EventValue = string | number | {error: Error};
+
+// TODO(dmfilippov): TS-fix-any use more specific type instead if possible
+export type EventDetails = any;
+
+export interface Timer {
+ reset(): this;
+ end(): this;
+ withMaximum(maximum: number): this;
+}
+
+export interface ReportingService {
+ reporter(
+ type: string,
+ category: string,
+ eventName: string,
+ eventValue?: EventValue,
+ eventDetails?: EventDetails,
+ opt_noLog?: boolean
+ ): void;
+
+ appStarted(): void;
+ onVisibilityChange(): void;
+ beforeLocationChanged(): void;
+ locationChanged(page: string): void;
+ dashboardDisplayed(): void;
+ changeDisplayed(): void;
+ changeFullyLoaded(): void;
+ diffViewDisplayed(): void;
+ diffViewFullyLoaded(): void;
+ diffViewContentDisplayed(): void;
+ fileListDisplayed(): void;
+ reportExtension(name: string): void;
+ pluginLoaded(name: string): void;
+ pluginsLoaded(pluginsList?: string[]): void;
+ /**
+ * Reset named timer.
+ */
+ time(name: string): void;
+ /**
+ * Finish named timer and report it to server.
+ */
+ timeEnd(name: string, eventDetails?: EventDetails): void;
+ /**
+ * Reports just line timeEnd, but additionally reports an average given a
+ * denominator and a separate reporiting name for the average.
+ *
+ * @param name Timing name.
+ * @param averageName Average timing name.
+ * @param denominator Number by which to divide the total to
+ * compute the average.
+ */
+ timeEndWithAverage(
+ name: string,
+ averageName: string,
+ denominator: number
+ ): void;
+ /**
+ * Get a timer object to for reporing a user timing. The start time will be
+ * the time that the object has been created, and the end time will be the
+ * time that the "end" method is called on the object.
+ */
+ getTimer(name: string): Timer;
+ /**
+ * Log timing information for an RPC.
+ *
+ * @param anonymizedUrl The URL of the RPC with tokens obfuscated.
+ * @param elapsed The time elapsed of the RPC.
+ */
+ reportRpcTiming(anonymizedUrl: string, elapsed: number): void;
+ reportLifeCycle(eventName: string, details: EventDetails): void;
+ reportInteraction(eventName: string, details: EventDetails): void;
+ /**
+ * A draft interaction was started. Update the time-betweeen-draft-actions
+ * timer.
+ */
+ recordDraftInteraction(): void;
+ reportErrorDialog(message: string): void;
+ setRepoName(repoName: string): void;
+}
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
new file mode 100644
index 0000000..111820b
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -0,0 +1,820 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {AppContext} from '../app-context';
+import {FlagsService} from '../flags/flags';
+import {
+ EventDetails,
+ EventValue,
+ ReportingService,
+ Timer,
+} from './gr-reporting';
+import {hasOwnProperty} from '../../utils/common-util';
+
+// Latency reporting constants.
+
+const TIMING = {
+ TYPE: 'timing-report',
+ CATEGORY: {
+ UI_LATENCY: 'UI Latency',
+ RPC: 'RPC Timing',
+ },
+ EVENT: {
+ APP_STARTED: 'App Started',
+ },
+};
+
+const LIFECYCLE = {
+ TYPE: 'lifecycle',
+ CATEGORY: {
+ DEFAULT: 'Default',
+ EXTENSION_DETECTED: 'Extension detected',
+ PLUGINS_INSTALLED: 'Plugins installed',
+ VISIBILITY: 'Visibility',
+ },
+};
+
+const INTERACTION = {
+ TYPE: 'interaction',
+ CATEGORY: {
+ DEFAULT: 'Default',
+ VISIBILITY: 'Visibility',
+ },
+};
+
+const NAVIGATION = {
+ TYPE: 'nav-report',
+ CATEGORY: {
+ LOCATION_CHANGED: 'Location Changed',
+ },
+ EVENT: {
+ PAGE: 'Page',
+ },
+};
+
+const ERROR = {
+ TYPE: 'error',
+ CATEGORY: {
+ EXCEPTION: 'exception',
+ ERROR_DIALOG: 'Error Dialog',
+ },
+};
+
+const TIMER = {
+ CHANGE_DISPLAYED: 'ChangeDisplayed',
+ CHANGE_LOAD_FULL: 'ChangeFullyLoaded',
+ DASHBOARD_DISPLAYED: 'DashboardDisplayed',
+ DIFF_VIEW_CONTENT_DISPLAYED: 'DiffViewOnlyContent',
+ DIFF_VIEW_DISPLAYED: 'DiffViewDisplayed',
+ DIFF_VIEW_LOAD_FULL: 'DiffViewFullyLoaded',
+ FILE_LIST_DISPLAYED: 'FileListDisplayed',
+ PLUGINS_LOADED: 'PluginsLoaded',
+ STARTUP_CHANGE_DISPLAYED: 'StartupChangeDisplayed',
+ STARTUP_CHANGE_LOAD_FULL: 'StartupChangeFullyLoaded',
+ STARTUP_DASHBOARD_DISPLAYED: 'StartupDashboardDisplayed',
+ STARTUP_DIFF_VIEW_CONTENT_DISPLAYED: 'StartupDiffViewOnlyContent',
+ STARTUP_DIFF_VIEW_DISPLAYED: 'StartupDiffViewDisplayed',
+ STARTUP_DIFF_VIEW_LOAD_FULL: 'StartupDiffViewFullyLoaded',
+ STARTUP_FILE_LIST_DISPLAYED: 'StartupFileListDisplayed',
+ WEB_COMPONENTS_READY: 'WebComponentsReady',
+ METRICS_PLUGIN_LOADED: 'MetricsPluginLoaded',
+};
+
+const STARTUP_TIMERS = {
+ [TIMER.PLUGINS_LOADED]: 0,
+ [TIMER.METRICS_PLUGIN_LOADED]: 0,
+ [TIMER.STARTUP_CHANGE_DISPLAYED]: 0,
+ [TIMER.STARTUP_CHANGE_LOAD_FULL]: 0,
+ [TIMER.STARTUP_DASHBOARD_DISPLAYED]: 0,
+ [TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED]: 0,
+ [TIMER.STARTUP_DIFF_VIEW_DISPLAYED]: 0,
+ [TIMER.STARTUP_DIFF_VIEW_LOAD_FULL]: 0,
+ [TIMER.STARTUP_FILE_LIST_DISPLAYED]: 0,
+ [TIMING.EVENT.APP_STARTED]: 0,
+ // WebComponentsReady timer is triggered from gr-router.
+ [TIMER.WEB_COMPONENTS_READY]: 0,
+};
+
+const DRAFT_ACTION_TIMER = 'TimeBetweenDraftActions';
+const DRAFT_ACTION_TIMER_MAX = 2 * 60 * 1000; // 2 minutes.
+const SLOW_RPC_THRESHOLD = 500;
+
+export function initErrorReporter(appContext: AppContext) {
+ const reportingService = appContext.reportingService;
+ // TODO(dmfilippo): TS-fix-any oldOnError - define correct type
+ const onError = function (
+ oldOnError: Function,
+ msg: string,
+ url: string,
+ line: number,
+ column: number,
+ error: Error
+ ) {
+ if (oldOnError) {
+ oldOnError(msg, url, line, column, error);
+ }
+ if (error) {
+ line = line || (error as any).lineNumber;
+ column = column || (error as any).columnNumber;
+ let shortenedErrorStack = msg;
+ if (error.stack) {
+ const errorStackLines = error.stack.split('\n');
+ shortenedErrorStack = errorStackLines
+ .slice(0, Math.min(3, errorStackLines.length))
+ .join('\n');
+ }
+ msg = shortenedErrorStack || error.toString();
+ }
+ const payload = {
+ url,
+ line,
+ column,
+ error,
+ };
+ reportingService.reporter(
+ ERROR.TYPE,
+ ERROR.CATEGORY.EXCEPTION,
+ msg,
+ payload
+ );
+ return true;
+ };
+ // TODO(dmfilippov): TS-fix-any unclear what is context
+ const catchErrors = function (opt_context?: any) {
+ const context = opt_context || window;
+ context.onerror = onError.bind(null, context.onerror);
+ context.addEventListener(
+ 'unhandledrejection',
+ (e: PromiseRejectionEvent) => {
+ const msg = e.reason.message;
+ const payload = {
+ error: e.reason,
+ };
+ reportingService.reporter(
+ ERROR.TYPE,
+ ERROR.CATEGORY.EXCEPTION,
+ msg,
+ payload
+ );
+ }
+ );
+ };
+
+ catchErrors();
+
+ // for testing
+ return {catchErrors};
+}
+
+export function initPerformanceReporter(appContext: AppContext) {
+ const reportingService = appContext.reportingService;
+ // PerformanceObserver interface is a browser API.
+ if (window.PerformanceObserver) {
+ const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || [];
+ // Safari doesn't support longtask yet
+ if (supportedEntryTypes.includes('longtask')) {
+ const catchLongJsTasks = new PerformanceObserver(list => {
+ for (const task of list.getEntries()) {
+ // We are interested in longtask longer than 200 ms (default is 50 ms)
+ if (task.duration > 200) {
+ reportingService.reporter(
+ TIMING.TYPE,
+ TIMING.CATEGORY.UI_LATENCY,
+ `Task ${task.name}`,
+ Math.round(task.duration),
+ {},
+ false
+ );
+ }
+ }
+ });
+ catchLongJsTasks.observe({entryTypes: ['longtask']});
+ }
+ }
+}
+
+export function initVisibilityReporter(appContext: AppContext) {
+ const reportingService = appContext.reportingService;
+ document.addEventListener('visibilitychange', () => {
+ reportingService.onVisibilityChange();
+ });
+}
+
+// Calculates the time of Gerrit being in a background tab. When Gerrit reports
+// a pageLoad metric it’s attached to its details for latency analysis.
+// It resets on locationChange.
+class HiddenDurationTimer {
+ public accHiddenDurationMs = 0;
+
+ public lastVisibleTimestampMs: number | null = null;
+
+ constructor() {
+ this.reset();
+ }
+
+ reset() {
+ this.accHiddenDurationMs = 0;
+ this.lastVisibleTimestampMs = 0;
+ }
+
+ onVisibilityChange() {
+ if (document.visibilityState === 'hidden') {
+ this.lastVisibleTimestampMs = now();
+ } else if (document.visibilityState === 'visible') {
+ if (this.lastVisibleTimestampMs !== null) {
+ this.accHiddenDurationMs += now() - this.lastVisibleTimestampMs;
+ // Set to null for guarding against two 'visible' events in a row.
+ this.lastVisibleTimestampMs = null;
+ }
+ }
+ }
+
+ get hiddenDurationMs() {
+ if (
+ document.visibilityState === 'hidden' &&
+ this.lastVisibleTimestampMs !== null
+ ) {
+ return this.accHiddenDurationMs + now() - this.lastVisibleTimestampMs;
+ }
+ return this.accHiddenDurationMs;
+ }
+}
+
+export function now() {
+ return Math.round(window.performance.now());
+}
+
+type PeformanceTimingEventName = keyof Omit<PerformanceTiming, 'toJSON'>;
+
+interface EventInfo {
+ type: string;
+ category: string;
+ name: string;
+ value?: EventValue;
+ eventStart: number;
+ eventDetails?: string;
+ repoName?: string;
+ inBackgroundTab?: boolean;
+ enabledExperiments?: string;
+}
+
+interface PageLoadDetails {
+ rpcList: SlowRpcCall[];
+ hiddenDurationMs: number;
+ screenSize?: {width: number; height: number};
+ viewport?: {width: number; height: number};
+ usedJSHeapSizeMb?: number;
+}
+
+interface SlowRpcCall {
+ anonymizedUrl: string;
+ elapsed: number;
+}
+
+type PendingReportInfo = [EventInfo, boolean | undefined];
+
+export class GrReporting implements ReportingService {
+ private readonly _flagsService: FlagsService;
+
+ private readonly _baselines = STARTUP_TIMERS;
+
+ private _reportRepoName: string | undefined;
+
+ private _timers: {timeBetweenDraftActions: Timer | null} = {
+ timeBetweenDraftActions: null,
+ };
+
+ private _pending: PendingReportInfo[] = [];
+
+ private _slowRpcList: SlowRpcCall[] = [];
+
+ public readonly hiddenDurationTimer = new HiddenDurationTimer();
+
+ constructor(flagsService: FlagsService) {
+ this._flagsService = flagsService;
+ }
+
+ private get performanceTiming() {
+ return window.performance.timing;
+ }
+
+ private get slowRpcSnapshot() {
+ return (this._slowRpcList || []).slice();
+ }
+
+ private _arePluginsLoaded() {
+ return (
+ this._baselines && !hasOwnProperty(this._baselines, TIMER.PLUGINS_LOADED)
+ );
+ }
+
+ private _isMetricsPluginLoaded() {
+ return (
+ this._arePluginsLoaded() ||
+ (this._baselines &&
+ !hasOwnProperty(this._baselines, TIMER.METRICS_PLUGIN_LOADED))
+ );
+ }
+
+ /**
+ * Reporter reports events. Events will be queued if metrics plugin is not
+ * yet installed.
+ *
+ * @param {string} type
+ * @param {string} category
+ * @param {string} eventName
+ * @param {string|number} eventValue
+ * @param {Object} eventDetails
+ * @param {boolean|undefined} opt_noLog If true, the event will not be
+ * logged to the JS console.
+ */
+ reporter(
+ type: string,
+ category: string,
+ eventName: string,
+ eventValue?: EventValue,
+ eventDetails?: EventDetails,
+ opt_noLog?: boolean
+ ) {
+ const eventInfo = this._createEventInfo(
+ type,
+ category,
+ eventName,
+ eventValue,
+ eventDetails
+ );
+ if (type === ERROR.TYPE && category === ERROR.CATEGORY.EXCEPTION) {
+ console.error((eventValue && (eventValue as any).error) || eventName);
+ }
+
+ // We report events immediately when metrics plugin is loaded
+ if (this._isMetricsPluginLoaded() && !this._pending.length) {
+ this._reportEvent(eventInfo, opt_noLog);
+ } else {
+ // We cache until metrics plugin is loaded
+ this._pending.push([eventInfo, opt_noLog]);
+ if (this._isMetricsPluginLoaded()) {
+ this._pending.forEach(([eventInfo, opt_noLog]) => {
+ this._reportEvent(eventInfo, opt_noLog);
+ });
+ this._pending = [];
+ }
+ }
+ }
+
+ private _reportEvent(eventInfo: EventInfo, opt_noLog?: boolean) {
+ const {type, value, name} = eventInfo;
+ document.dispatchEvent(new CustomEvent(type, {detail: eventInfo}));
+ if (opt_noLog) {
+ return;
+ }
+ if (type !== ERROR.TYPE) {
+ if (value !== undefined) {
+ console.info(`Reporting: ${name}: ${value}`);
+ } else {
+ console.info(`Reporting: ${name}`);
+ }
+ }
+ }
+
+ private _createEventInfo(
+ type: string,
+ category: string,
+ name: string,
+ value?: EventValue,
+ eventDetails?: EventDetails
+ ): EventInfo {
+ const eventInfo: EventInfo = {
+ type,
+ category,
+ name,
+ value,
+ eventStart: now(),
+ };
+
+ if (
+ typeof eventDetails === 'object' &&
+ Object.entries(eventDetails).length !== 0
+ ) {
+ eventInfo.eventDetails = JSON.stringify(eventDetails);
+ }
+
+ if (this._reportRepoName) {
+ eventInfo.repoName = this._reportRepoName;
+ }
+
+ const isInBackgroundTab = document.visibilityState === 'hidden';
+ if (isInBackgroundTab !== undefined) {
+ eventInfo.inBackgroundTab = isInBackgroundTab;
+ }
+
+ if (this._flagsService.enabledExperiments.length) {
+ eventInfo.enabledExperiments = JSON.stringify(
+ this._flagsService.enabledExperiments
+ );
+ }
+
+ return eventInfo;
+ }
+
+ /**
+ * User-perceived app start time, should be reported when the app is ready.
+ */
+ appStarted() {
+ this.timeEnd(TIMING.EVENT.APP_STARTED);
+ this._reportNavResTimes();
+ }
+
+ onVisibilityChange() {
+ this.hiddenDurationTimer.onVisibilityChange();
+ const eventName = `Visibility changed to ${document.visibilityState}`;
+ this.reporter(
+ LIFECYCLE.TYPE,
+ LIFECYCLE.CATEGORY.VISIBILITY,
+ eventName,
+ undefined,
+ {
+ hiddenDurationMs: this.hiddenDurationTimer.hiddenDurationMs,
+ },
+ true
+ );
+ }
+
+ /**
+ * Browser's navigation and resource timings
+ */
+ private _reportNavResTimes() {
+ const perfEvents = Object.keys(this.performanceTiming.toJSON());
+ perfEvents.forEach(eventName =>
+ this._reportPerformanceTiming(eventName as PeformanceTimingEventName)
+ );
+ }
+
+ private _reportPerformanceTiming(
+ eventName: PeformanceTimingEventName,
+ eventDetails?: EventDetails
+ ) {
+ const eventTiming = this.performanceTiming[eventName];
+ if (eventTiming > 0) {
+ const elapsedTime = eventTiming - this.performanceTiming.navigationStart;
+ // NavResTime - Navigation and resource timings.
+ this.reporter(
+ TIMING.TYPE,
+ TIMING.CATEGORY.UI_LATENCY,
+ `NavResTime - ${eventName}`,
+ elapsedTime,
+ eventDetails,
+ true
+ );
+ }
+ }
+
+ beforeLocationChanged() {
+ for (const prop of Object.keys(this._baselines)) {
+ delete this._baselines[prop];
+ }
+ this.time(TIMER.CHANGE_DISPLAYED);
+ this.time(TIMER.CHANGE_LOAD_FULL);
+ this.time(TIMER.DASHBOARD_DISPLAYED);
+ this.time(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
+ this.time(TIMER.DIFF_VIEW_DISPLAYED);
+ this.time(TIMER.DIFF_VIEW_LOAD_FULL);
+ this.time(TIMER.FILE_LIST_DISPLAYED);
+ this._reportRepoName = undefined;
+ // reset slow rpc list since here start page loads which report these rpcs
+ this._slowRpcList = [];
+ this.hiddenDurationTimer.reset();
+ }
+
+ locationChanged(page: string) {
+ this.reporter(
+ NAVIGATION.TYPE,
+ NAVIGATION.CATEGORY.LOCATION_CHANGED,
+ NAVIGATION.EVENT.PAGE,
+ page
+ );
+ }
+
+ dashboardDisplayed() {
+ if (hasOwnProperty(this._baselines, TIMER.STARTUP_DASHBOARD_DISPLAYED)) {
+ this.timeEnd(TIMER.STARTUP_DASHBOARD_DISPLAYED, this._pageLoadDetails());
+ } else {
+ this.timeEnd(TIMER.DASHBOARD_DISPLAYED, this._pageLoadDetails());
+ }
+ }
+
+ changeDisplayed() {
+ if (hasOwnProperty(this._baselines, TIMER.STARTUP_CHANGE_DISPLAYED)) {
+ this.timeEnd(TIMER.STARTUP_CHANGE_DISPLAYED, this._pageLoadDetails());
+ } else {
+ this.timeEnd(TIMER.CHANGE_DISPLAYED, this._pageLoadDetails());
+ }
+ }
+
+ changeFullyLoaded() {
+ if (hasOwnProperty(this._baselines, TIMER.STARTUP_CHANGE_LOAD_FULL)) {
+ this.timeEnd(TIMER.STARTUP_CHANGE_LOAD_FULL);
+ } else {
+ this.timeEnd(TIMER.CHANGE_LOAD_FULL);
+ }
+ }
+
+ diffViewDisplayed() {
+ if (hasOwnProperty(this._baselines, TIMER.STARTUP_DIFF_VIEW_DISPLAYED)) {
+ this.timeEnd(TIMER.STARTUP_DIFF_VIEW_DISPLAYED, this._pageLoadDetails());
+ } else {
+ this.timeEnd(TIMER.DIFF_VIEW_DISPLAYED, this._pageLoadDetails());
+ }
+ }
+
+ diffViewFullyLoaded() {
+ if (hasOwnProperty(this._baselines, TIMER.STARTUP_DIFF_VIEW_LOAD_FULL)) {
+ this.timeEnd(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL);
+ } else {
+ this.timeEnd(TIMER.DIFF_VIEW_LOAD_FULL);
+ }
+ }
+
+ diffViewContentDisplayed() {
+ if (
+ hasOwnProperty(this._baselines, TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED)
+ ) {
+ this.timeEnd(TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED);
+ } else {
+ this.timeEnd(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
+ }
+ }
+
+ fileListDisplayed() {
+ if (hasOwnProperty(this._baselines, TIMER.STARTUP_FILE_LIST_DISPLAYED)) {
+ this.timeEnd(TIMER.STARTUP_FILE_LIST_DISPLAYED);
+ } else {
+ this.timeEnd(TIMER.FILE_LIST_DISPLAYED);
+ }
+ }
+
+ private _pageLoadDetails(): PageLoadDetails {
+ const details: PageLoadDetails = {
+ rpcList: this.slowRpcSnapshot,
+ hiddenDurationMs: this.hiddenDurationTimer.accHiddenDurationMs,
+ };
+
+ if (window.screen) {
+ details.screenSize = {
+ width: window.screen.width,
+ height: window.screen.height,
+ };
+ }
+
+ if (document && document.documentElement) {
+ details.viewport = {
+ width: document.documentElement.clientWidth,
+ height: document.documentElement.clientHeight,
+ };
+ }
+
+ if (window.performance && window.performance.memory) {
+ const toMb = (bytes: number) =>
+ Math.round((bytes / (1024 * 1024)) * 100) / 100;
+ details.usedJSHeapSizeMb = toMb(window.performance.memory.usedJSHeapSize);
+ }
+
+ details.hiddenDurationMs = this.hiddenDurationTimer.hiddenDurationMs;
+ return details;
+ }
+
+ reportExtension(name: string) {
+ this.reporter(LIFECYCLE.TYPE, LIFECYCLE.CATEGORY.EXTENSION_DETECTED, name);
+ }
+
+ pluginLoaded(name: string) {
+ if (name.startsWith('metrics-')) {
+ this.timeEnd(TIMER.METRICS_PLUGIN_LOADED);
+ }
+ }
+
+ pluginsLoaded(pluginsList?: string[]) {
+ this.timeEnd(TIMER.PLUGINS_LOADED);
+ this.reporter(
+ LIFECYCLE.TYPE,
+ LIFECYCLE.CATEGORY.PLUGINS_INSTALLED,
+ LIFECYCLE.CATEGORY.PLUGINS_INSTALLED,
+ undefined,
+ {pluginsList: pluginsList || []},
+ true
+ );
+ }
+
+ /**
+ * Reset named timer.
+ */
+ time(name: string) {
+ this._baselines[name] = now();
+ window.performance.mark(`${name}-start`);
+ }
+
+ /**
+ * Finish named timer and report it to server.
+ */
+ timeEnd(name: string, eventDetails?: EventDetails) {
+ if (!hasOwnProperty(this._baselines, name)) {
+ return;
+ }
+ const baseTime = this._baselines[name];
+ delete this._baselines[name];
+ this._reportTiming(name, now() - baseTime, eventDetails);
+
+ // Finalize the interval. Either from a registered start mark or
+ // the navigation start time (if baseTime is 0).
+ if (baseTime !== 0) {
+ window.performance.measure(name, `${name}-start`);
+ } else {
+ // Microsft Edge does not handle the 2nd param correctly
+ // (if undefined).
+ window.performance.measure(name);
+ }
+ }
+
+ /**
+ * Reports just line timeEnd, but additionally reports an average given a
+ * denominator and a separate reporiting name for the average.
+ *
+ * @param name Timing name.
+ * @param averageName Average timing name.
+ * @param denominator Number by which to divide the total to
+ * compute the average.
+ */
+ timeEndWithAverage(name: string, averageName: string, denominator: number) {
+ if (!hasOwnProperty(this._baselines, name)) {
+ return;
+ }
+ const baseTime = this._baselines[name];
+ this.timeEnd(name);
+
+ // Guard against division by zero.
+ if (!denominator) {
+ return;
+ }
+ const time = now() - baseTime;
+ this._reportTiming(averageName, time / denominator);
+ }
+
+ /**
+ * Send a timing report with an arbitrary time value.
+ *
+ * @param name Timing name.
+ * @param time The time to report as an integer of milliseconds.
+ * @param eventDetails non sensitive details
+ */
+ private _reportTiming(
+ name: string,
+ time: number,
+ eventDetails?: EventDetails
+ ) {
+ this.reporter(
+ TIMING.TYPE,
+ TIMING.CATEGORY.UI_LATENCY,
+ name,
+ time,
+ eventDetails
+ );
+ }
+
+ /**
+ * Get a timer object to for reporing a user timing. The start time will be
+ * the time that the object has been created, and the end time will be the
+ * time that the "end" method is called on the object.
+ */
+ getTimer(name: string): Timer {
+ let called = false;
+ let start: number;
+ let max: number | null = null;
+
+ const timer: Timer = {
+ // Clear the timer and reset the start time.
+ reset: () => {
+ called = false;
+ start = now();
+ return timer;
+ },
+
+ // Stop the timer and report the intervening time.
+ end: () => {
+ if (called) {
+ throw new Error(`Timer for "${name}" already ended.`);
+ }
+ called = true;
+ const time = now() - start;
+
+ // If a maximum is specified and the time exceeds it, do not report.
+ if (max && time > max) {
+ return timer;
+ }
+
+ this._reportTiming(name, time);
+ return timer;
+ },
+
+ // Set a maximum reportable time. If a maximum is set and the timer is
+ // ended after the specified amount of time, the value is not reported.
+ withMaximum(maximum) {
+ max = maximum;
+ return timer;
+ },
+ };
+
+ // The timer is initialized to its creation time.
+ return timer.reset();
+ }
+
+ /**
+ * Log timing information for an RPC.
+ *
+ * @param anonymizedUrl The URL of the RPC with tokens obfuscated.
+ * @param elapsed The time elapsed of the RPC.
+ */
+ reportRpcTiming(anonymizedUrl: string, elapsed: number) {
+ this.reporter(
+ TIMING.TYPE,
+ TIMING.CATEGORY.RPC,
+ 'RPC-' + anonymizedUrl,
+ elapsed,
+ {},
+ true
+ );
+ if (elapsed >= SLOW_RPC_THRESHOLD) {
+ this._slowRpcList.push({anonymizedUrl, elapsed});
+ }
+ }
+
+ reportLifeCycle(eventName: string, details: EventDetails) {
+ this.reporter(
+ LIFECYCLE.TYPE,
+ LIFECYCLE.CATEGORY.DEFAULT,
+ eventName,
+ undefined,
+ details,
+ true
+ );
+ }
+
+ reportInteraction(eventName: string, details: EventDetails) {
+ this.reporter(
+ INTERACTION.TYPE,
+ INTERACTION.CATEGORY.DEFAULT,
+ eventName,
+ undefined,
+ details,
+ true
+ );
+ }
+
+ /**
+ * A draft interaction was started. Update the time-betweeen-draft-actions
+ * timer.
+ */
+ recordDraftInteraction() {
+ // If there is no timer defined, then this is the first interaction.
+ // Set up the timer so that it's ready to record the intervening time when
+ // called again.
+ const timer = this._timers.timeBetweenDraftActions;
+ if (!timer) {
+ // Create a timer with a maximum length.
+ this._timers.timeBetweenDraftActions = this.getTimer(
+ DRAFT_ACTION_TIMER
+ ).withMaximum(DRAFT_ACTION_TIMER_MAX);
+ return;
+ }
+
+ // Mark the time and reinitialize the timer.
+ timer.end().reset();
+ }
+
+ reportErrorDialog(message: string) {
+ this.reporter(
+ ERROR.TYPE,
+ ERROR.CATEGORY.ERROR_DIALOG,
+ 'ErrorDialog: ' + message,
+ {error: new Error(message)}
+ );
+ }
+
+ setRepoName(repoName: string) {
+ this._reportRepoName = repoName;
+ }
+}
+
+export const DEFAULT_STARTUP_TIMERS = {...STARTUP_TIMERS};
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.js b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.js
index 7c70e10..73f8580 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.js
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.js
@@ -16,7 +16,7 @@
*/
import '../../test/common-test-setup-karma.js';
-import {GrReporting} from './gr-reporting.js';
+import {GrReporting} from './gr-reporting_impl.js';
import {grReportingMock} from './gr-reporting_mock.js';
suite('gr-reporting_mock tests', () => {
test('mocks all public methods', () => {
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js
index 1e50766..08e4a55 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js
@@ -16,7 +16,7 @@
*/
import '../../test/common-test-setup-karma.js';
-import {GrReporting, DEFAULT_STARTUP_TIMERS, initErrorReporter} from './gr-reporting.js';
+import {GrReporting, DEFAULT_STARTUP_TIMERS, initErrorReporter} from './gr-reporting_impl.js';
import {appContext} from '../app-context.js';
suite('gr-reporting tests', () => {
let service;
diff --git a/polygerrit-ui/app/tsconfig.json b/polygerrit-ui/app/tsconfig.json
index efe575a..bc6c2df 100644
--- a/polygerrit-ui/app/tsconfig.json
+++ b/polygerrit-ui/app/tsconfig.json
@@ -36,7 +36,8 @@
/* Advanced Options */
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
- "incremental": true
+ "incremental": true,
+ "experimentalDecorators": true
},
// With the * pattern (without an extension), only supported files
// are included. The supported files are .ts, .tsx, .d.ts.
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
new file mode 100644
index 0000000..a8fc01f
--- /dev/null
+++ b/polygerrit-ui/app/types/common.ts
@@ -0,0 +1,417 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ ChangeStatus,
+ FileInfoStatus,
+ GpgKeyInfoStatus,
+ ProblemInfoStatus,
+ RequirementStatus,
+ ReviewerState,
+ RevisionKind,
+} from '../constants/constants';
+
+export type BrandType<T, BrandName extends string> = T &
+ {[__brand in BrandName]: never};
+
+export type PatchSetNum = BrandType<'edit' | number, '_patchSet'>;
+export type ChangeId = BrandType<string, '_changeId'>;
+export type ChangeMessageId = BrandType<string, '_changeMessageId'>;
+export type LegacyChangeId = BrandType<number, '_legacyChangeId'>;
+export type NumericChangeId = BrandType<number, '_numericChangeId'>;
+export type ProjectName = BrandType<string, '_projectName'>;
+export type TopicName = BrandType<string, '_topicName'>;
+export type AccountId = BrandType<number, '_accountId'>;
+export type HttpMethod = BrandType<string, '_httpMethod'>;
+export type GitRef = BrandType<string, '_gitRef'>;
+export type RequirementType = BrandType<string, '_requirementType'>;
+export type TrackingId = BrandType<string, '_trackingId'>;
+export type ReviewInputTag = BrandType<string, '_reviewInputTag'>;
+
+// The 8-char hex GPG key ID.
+export type GpgKeyId = BrandType<string, '_gpgKeyId'>;
+
+// The 40-char (plus spaces) hex GPG key fingerprint
+export type GpgKeyFingerprint = BrandType<string, '_gpgKeyFingerprint'>;
+
+// OpenPGP User IDs (https://tools.ietf.org/html/rfc4880#section-5.11).
+export type OpenPgpUserIds = BrandType<string, '_openPgpUserIds'>;
+
+// This ID is equal to the numeric ID of the change that triggered the
+// submission. If the change that triggered the submission also has a topic, it
+// will be "<id>-<topic>" of the change that triggered the submission
+// The callers must not rely on the format of the submission ID.
+export type ChangeSubmissionId = BrandType<
+ string | number,
+ '_changeSubmissionId'
+>;
+
+// The refs/heads/ prefix is omitted in Branch name
+export type BranchName = BrandType<string, '_branchName'>;
+
+// The ID of the change in the format "'<project>~<branch>~<Change-Id>'"
+export type ChangeInfoId = BrandType<string, '_changeInfoId'>;
+export type Hashtag = BrandType<string, '_hashtag'>;
+export type StarLabel = BrandType<string, '_startLabel'>;
+export type SubmitType = BrandType<string, '_submitType'>;
+export type CommitId = BrandType<string, '_commitId'>;
+
+// The timezone offset from UTC in minutes
+export type TimezoneOffset = BrandType<number, '_timezoneOffset'>;
+
+// Timestamps are given in UTC and have the format
+// "'yyyy-mm-dd hh:mm:ss.fffffffff'"
+// where "'ffffffffff'" represents nanoseconds.
+export type Timestamp = BrandType<string, '_timestamp'>;
+
+export type IdToAttentionSetMap = {[accountId: string]: AttentionSetInfo};
+export type LabelNameToInfoMap = {[labelName: string]: LabelInfo};
+
+// The map maps the values (“-2”, “-1”, " `0`", “+1”, “+2”) to the value descriptions.
+export type LabelValueToDescriptionMap = {[labelValue: string]: string};
+
+/**
+ * The LabelInfo entity contains information about a label on a change, always corresponding to the current patch set.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#label-info
+ */
+type LabelInfo = QuickLabelInfo | DetailedLabelInfo;
+
+interface LabelCommonInfo {
+ optional?: boolean; // not set if false
+}
+
+export interface QuickLabelInfo extends LabelCommonInfo {
+ approved?: AccountInfo;
+ rejected?: AccountInfo;
+ recommended?: AccountInfo;
+ disliked?: AccountInfo;
+ blocking?: boolean; // not set if false
+ value?: number; // The voting value of the user who recommended/disliked this label on the change if it is not “+1”/“-1”.
+ default_value?: number;
+}
+
+export interface DetailedLabelInfo extends LabelCommonInfo {
+ all?: ApprovalInfo[];
+ values?: LabelValueToDescriptionMap; // A map of all values that are allowed for this label
+}
+
+/**
+ * The ChangeInfo entity contains information about a change.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
+ */
+export interface ChangeInfo {
+ id: ChangeInfoId;
+ project: ProjectName;
+ branch: BranchName;
+ topic?: TopicName;
+ attention_set?: IdToAttentionSetMap;
+ assignee?: AccountInfo;
+ hashtags?: Hashtag[];
+ change_id: ChangeId;
+ subject: string;
+ status: ChangeStatus;
+ created: Timestamp;
+ updated: Timestamp;
+ submitted?: Timestamp;
+ submitter: AccountInfo;
+ starred?: boolean; // not set if false
+ stars?: StarLabel[];
+ reviewed?: boolean; // not set if false
+ submit_type?: SubmitType;
+ mergeable?: boolean;
+ submittable?: boolean;
+ insertions: number; // Number of inserted lines
+ deletions: number; // Number of deleted lines
+ total_comment_count?: number;
+ unresolved_comment_count?: number;
+ _number: LegacyChangeId;
+ owner: AccountInfo;
+ actions?: ActionInfo[];
+ requirements?: Requirement[];
+ labels?: LabelInfo[];
+ permitted_labels?: LabelNameToInfoMap;
+ removable_reviewers?: AccountInfo[];
+ reviewers?: AccountInfo[];
+ pending_reviewers?: AccountInfo[];
+ reviewer_updates?: ReviewerUpdateInfo[];
+ messages?: ChangeMessageInfo[];
+ current_revision?: CommitId;
+ revisions?: {[revisionId: string]: RevisionInfo};
+ tracking_ids?: TrackingIdInfo[];
+ _more_changes?: boolean; // not set if false
+ problems?: ProblemInfo[];
+ is_private?: boolean; // not set if false
+ work_in_progress?: boolean; // not set if false
+ has_review_started?: boolean; // not set if false
+ revert_of?: NumericChangeId;
+ submission_id?: ChangeSubmissionId;
+ cherry_pick_of_change?: NumericChangeId;
+ cherry_pick_of_patch_set?: PatchSetNum;
+ contains_git_conflicts?: boolean;
+}
+
+/**
+ * The AccountInfo entity contains information about an account.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#account-info
+ */
+export interface AccountInfo {
+ _account_id: AccountId;
+ name?: string;
+ display_name?: string;
+ email?: string;
+ secondary_emails?: string[];
+ username?: string;
+ avatars?: AvatarInfo[];
+ _more_accounts?: boolean; // not set if false
+ status?: string; // status message of the account
+ inactive?: boolean; // not set if false
+}
+
+/**
+ * The ActionInfo entity describes a REST API call the client canmake to
+ * manipulate a resource. These are frequently implemented by plugins and may
+ * be discovered at runtime.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#action-info
+ */
+export interface ActionInfo {
+ method?: HttpMethod; // Most actions use POST, PUT or DELETE to cause state changes.
+ label?: string; // Short title to display to a user describing the action
+ title?: string; // Longer text to display describing the action
+ enabled?: boolean; // not set if false
+}
+
+/**
+ * The Requirement entity contains information about a requirement relative to
+ * a change.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#requirement
+ */
+export interface Requirement {
+ status: RequirementStatus;
+ fallbackText: string; // A human readable reason
+ type: RequirementType;
+}
+
+/**
+ * The ReviewerUpdateInfo entity contains information about updates tochange’s
+ * reviewers set.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#review-update-info
+ */
+export interface ReviewerUpdateInfo {
+ updated: Timestamp;
+ updated_by: AccountInfo;
+ reviewer: AccountInfo;
+ state: ReviewerState;
+}
+
+/**
+ * The ChangeMessageInfo entity contains information about a messageattached
+ * to a change.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-message-info
+ */
+export interface ChangeMessageInfo {
+ id: ChangeMessageId;
+ author?: AccountInfo;
+ real_author?: AccountInfo;
+ date: Timestamp;
+ message: string;
+ tag?: ReviewInputTag;
+ _revision_number?: PatchSetNum;
+}
+
+/**
+ * The RevisionInfo entity contains information about a patch set.Not all
+ * fields are returned by default. Additional fields can be obtained by
+ * adding o parameters as described in Query Changes.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#revision-info
+ */
+export interface RevisionInfo {
+ kind: RevisionKind;
+ _number: PatchSetNum;
+ created: Timestamp;
+ uploader: AccountInfo;
+ ref: GitRef;
+ fetch?: {[protocol: string]: FetchInfo};
+ commit?: CommitInfo;
+ files?: {[filename: string]: FileInfo};
+ actions?: ActionInfo[];
+ reviewed?: boolean;
+ commit_with_footers?: boolean;
+ push_certificate?: PushCertificateInfo;
+ description?: string;
+}
+
+/**
+ * The TrackingIdInfo entity describes a reference to an external tracking
+ * system.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#tracking-id-info
+ */
+export interface TrackingIdInfo {
+ system: string;
+ id: TrackingId;
+}
+
+/**
+ * The ProblemInfo entity contains a description of a potential consistency
+ * problem with a change. These are not related to the code review process,
+ * but rather indicate some inconsistency in Gerrit’s database or repository
+ * metadata related to the enclosing change.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#problem-info
+ */
+export interface ProblemInfo {
+ message: string;
+ status?: ProblemInfoStatus; // Only set if a fix was attempted
+ outcome?: string;
+}
+
+/**
+ * The AttentionSetInfo entity contains details of users that are in the attention set.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#attention-set-info
+ */
+export interface AttentionSetInfo {
+ account: AccountInfo;
+ last_update: Timestamp;
+}
+
+/**
+ * The ApprovalInfo entity contains information about an approval from auser
+ * for a label on a change.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#approval-info
+ */
+export interface ApprovalInfo extends AccountInfo {
+ value?: string;
+ permitted_voting_range?: VotingRangeInfo;
+ date?: Timestamp;
+ tag?: ReviewInputTag;
+ post_submit?: boolean; // not set if false
+}
+
+/**
+ * The AvartarInfo entity contains information about an avatar image ofan
+ * account.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#avatar-info
+ */
+export interface AvatarInfo {
+ url: string;
+ height: number;
+ width: number;
+}
+
+/**
+ * The FetchInfo entity contains information about how to fetch a patchset via
+ * a certain protocol.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#fetch-info
+ */
+export interface FetchInfo {
+ url: string;
+ ref: string;
+ commands?: {[commandName: string]: string};
+}
+
+/**
+ * The CommitInfo entity contains information about a commit.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#commit-info
+ */
+export interface CommitInfo {
+ commit?: CommitId;
+ parents: ParentCommitInfo[];
+ author: GitPersonInfo;
+ committer: GitPersonInfo;
+ subject: string;
+ message: string;
+ web_links?: WebLinkInfo[];
+}
+
+/**
+ * The parent commits of this commit as a list of CommitInfo entities.
+ * In each parent only the commit and subject fields are populated.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#commit-info
+ */
+export interface ParentCommitInfo {
+ commit: CommitId;
+ subject: string;
+}
+
+/**
+ * The FileInfo entity contains information about a file in a patch set.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#file-info
+ */
+export interface FileInfo {
+ status?: FileInfoStatus;
+ binary?: boolean; // not set if false
+ old_path?: string;
+ lines_inserted?: number;
+ lines_deleted?: number;
+ size_delta: number; // in bytes
+ size: number; // in bytes
+}
+
+/**
+ * The PushCertificateInfo entity contains information about a pushcertificate
+ * provided when the user pushed for review with git push
+ * --signed HEAD:refs/for/<branch>. Only used when signed push is
+ * enabled on the server.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#push-certificate-info
+ */
+export interface PushCertificateInfo {
+ certificate: string;
+ key: GpgKeyInfo;
+}
+
+/**
+ * The GpgKeyInfo entity contains information about a GPG public key.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#gpg-key-info
+ */
+export interface GpgKeyInfo {
+ id?: GpgKeyId;
+ fingerprint?: GpgKeyFingerprint;
+ user_ids?: OpenPgpUserIds[];
+ key?: string; // ASCII armored public key material
+ status?: GpgKeyInfoStatus;
+ problems?: string[];
+}
+
+/**
+ * The GitPersonInfo entity contains information about theauthor/committer of
+ * a commit.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#git-person-info
+ */
+export interface GitPersonInfo {
+ name: string;
+ email: string;
+ date: Timestamp;
+ tz: TimezoneOffset;
+}
+
+/**
+ * The WebLinkInfo entity describes a link to an external site.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#web-link-info
+ */
+export interface WebLinkInfo {
+ name: string;
+ url: string;
+ image_url: string;
+}
+
+/**
+ * The VotingRangeInfo entity describes the continuous voting range from minto
+ * max values.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#voting-range-info
+ */
+export interface VotingRangeInfo {
+ min: number;
+ max: number;
+}
diff --git a/polygerrit-ui/app/types/globals.ts b/polygerrit-ui/app/types/globals.ts
index ac2ae7c..116ae9e 100644
--- a/polygerrit-ui/app/types/globals.ts
+++ b/polygerrit-ui/app/types/globals.ts
@@ -20,4 +20,14 @@
interface Window {
CANONICAL_PATH?: string;
}
+
+ interface Performance {
+ // typescript doesn't know about the memory property.
+ // Define it here, so it can be used everywhere
+ memory?: {
+ jsHeapSizeLimit: number;
+ totalJSHeapSize: number;
+ usedJSHeapSize: number;
+ };
+ }
}
diff --git a/polygerrit-ui/app/utils/common-util.ts b/polygerrit-ui/app/utils/common-util.ts
new file mode 100644
index 0000000..a7aca4c
--- /dev/null
+++ b/polygerrit-ui/app/utils/common-util.ts
@@ -0,0 +1,33 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @fileoverview Functions in this file contains some widely used
+ * code patterns. If you noticed a repeated code and none of the existing util
+ * files are appropriate for it - you can wrap the code in a function and put it
+ * here. If you notice that several functions can be group together - create
+ * a separate util file for them.
+ */
+
+/**
+ * Wrapper for the Object.prototype.hasOwnProperty method
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function hasOwnProperty(obj: any, prop: PropertyKey) {
+ // Typescript rules don't allow to use obj.hasOwnProperty directly
+ return Object.prototype.hasOwnProperty.call(obj, prop);
+}
diff --git a/polygerrit-ui/app/utils/common-util_test.js b/polygerrit-ui/app/utils/common-util_test.js
new file mode 100644
index 0000000..60c0b0a
--- /dev/null
+++ b/polygerrit-ui/app/utils/common-util_test.js
@@ -0,0 +1,44 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../test/common-test-setup-karma.js';
+import {hasOwnProperty} from './common-util.js';
+
+suite('common-util tests', () => {
+ suite('hasOwnProperty', () => {
+ test('object with the default prototype', () => {
+ const obj = {
+ 'abc': 3,
+ 'name with spaces': 5,
+ };
+ assert.isTrue(hasOwnProperty(obj, 'abc'));
+ assert.isTrue(hasOwnProperty(obj, 'name with spaces'));
+ assert.isFalse(hasOwnProperty(obj, 'def'));
+ });
+ test('object prototype has overriden hasOwnProperty', () => {
+ const F = function() {
+ this.abc = 23;
+ };
+ F.prototype.hasOwnProperty = function(key) {
+ return true;
+ };
+ const obj = new F();
+ assert.isTrue(hasOwnProperty(obj, 'abc'));
+ assert.isFalse(hasOwnProperty(obj, 'def'));
+ });
+ });
+});
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index 8fb3eea..45e0ea7 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -2,6 +2,13 @@
# yarn lockfile v1
+"@polymer/decorators@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@polymer/decorators/-/decorators-3.0.0.tgz#e4212ac976d9abd1210f560b6e1be4165c1c0183"
+ integrity sha512-qh+VID9nDV9q3ABvIfWgm7/+udl7v2HKsMLPXFm8tj1fI7qr7yWJMFwS3xWBkMmuNPtmkS8MDP0vqLAQIEOWzg==
+ dependencies:
+ "@polymer/polymer" "^3.0.5"
+
"@polymer/font-roboto-local@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@polymer/font-roboto-local/-/font-roboto-local-3.0.2.tgz#563cd6cabbcaef54999d654c0f3d476bcc49ce58"
@@ -13,9 +20,9 @@
integrity sha512-tx5TauYSmzsIvmSqepUPDYbs4/Ejz2XbZ1IkD7JEGqkdNUJlh+9KU85G56Tfdk/xjEZ8zorFfN09OSwiMrIQWA==
"@polymer/iron-a11y-announcer@^3.0.0-pre.26":
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/@polymer/iron-a11y-announcer/-/iron-a11y-announcer-3.0.2.tgz#730dd36ccb2e042ecd5160ba439c2bf2f8a97412"
- integrity sha512-LqnMF39mXyxSSRbTHRzGbcJS8nU0NVTo2raBOgOlpxw5yfGJUVcwaTJ/qy5NtWCZLRfa4suycf0oAkuUjHTXHQ==
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@polymer/iron-a11y-announcer/-/iron-a11y-announcer-3.1.0.tgz#3d3712a165070ed3cdfc39e54f95515c913c9613"
+ integrity sha512-lc5i4NKB8kSQHH0Hwu8WS3ym93m+J69OHJWSSBxwd17FI+h2wmgxDzeG9LI4ojMMck17/uc2pLe7g/UHt5/K/A==
dependencies:
"@polymer/polymer" "^3.0.0"
@@ -26,10 +33,10 @@
dependencies:
"@polymer/polymer" "^3.0.0"
-"@polymer/iron-autogrow-textarea@^3.0.0-pre.26", "@polymer/iron-autogrow-textarea@^3.0.1":
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/@polymer/iron-autogrow-textarea/-/iron-autogrow-textarea-3.0.1.tgz#0205d9c5ca16f3afd505f41e9037989707d59dce"
- integrity sha512-FgSL7APrOSL9Vu812sBCFlQ17hvnJsBAV2C2e1UAiaHhB+dyfLq8gGdGUpqVWuGJ50q4Y/49QwCNnLf85AdVYA==
+"@polymer/iron-autogrow-textarea@^3.0.0-pre.26", "@polymer/iron-autogrow-textarea@^3.0.3":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@polymer/iron-autogrow-textarea/-/iron-autogrow-textarea-3.0.3.tgz#b75dbebc23ce47d428a26156709d4a8a4c05823e"
+ integrity sha512-5r0VkWrIlm0JIp5E5wlnvkw7slK72lFRZXncmrsLZF+6n1dg2rI8jt7xpFzSmUWrqpcyXwyKaGaDvUjl3j4JLA==
dependencies:
"@polymer/iron-behaviors" "^3.0.0-pre.26"
"@polymer/iron-flex-layout" "^3.0.0-pre.26"
@@ -63,7 +70,7 @@
"@polymer/neon-animation" "^3.0.0-pre.26"
"@polymer/polymer" "^3.0.0"
-"@polymer/iron-fit-behavior@^3.0.0-pre.26", "@polymer/iron-fit-behavior@^3.0.1":
+"@polymer/iron-fit-behavior@^3.0.0-pre.26", "@polymer/iron-fit-behavior@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@polymer/iron-fit-behavior/-/iron-fit-behavior-3.0.2.tgz#2ec460d8a6b0151394b55631a72a68b92e14e2e0"
integrity sha512-JndryJYbBR3gSN5IlST4rCHsd01+OyvYpRO6z5Zd3C6u5V/m07TwAtcf3aXwZ8WBNt2eLG28OcvdSO7XR2v2pg==
@@ -127,7 +134,7 @@
dependencies:
"@polymer/polymer" "^3.0.0"
-"@polymer/iron-overlay-behavior@^3.0.0-pre.27", "@polymer/iron-overlay-behavior@^3.0.2":
+"@polymer/iron-overlay-behavior@^3.0.0-pre.27", "@polymer/iron-overlay-behavior@^3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@polymer/iron-overlay-behavior/-/iron-overlay-behavior-3.0.3.tgz#29c198e19e05bb2bcf7d86d3c11848cb93301d00"
integrity sha512-Q/Fp0+uOQQ145ebZ7T8Cxl4m1tUKYjyymkjcL2rXUm+aDQGb1wA1M1LYxUF5YBqd+9lipE0PTIiYwA2ZL/sznA==
@@ -227,10 +234,10 @@
"@polymer/paper-styles" "^3.0.0-pre.26"
"@polymer/polymer" "^3.0.0"
-"@polymer/paper-input@^3.0.2":
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/@polymer/paper-input/-/paper-input-3.2.0.tgz#a07dbc1b009bac97a5a86eccb57d99b17bd96285"
- integrity sha512-vYEBxq6LDR+QGDrAO/il0JNhCd+31TwSnv58MVV+ijaGKz1qAuSJw4NSsgF3lrXCwomqnpME19vbp2ktrcluVA==
+"@polymer/paper-input@^3.2.1":
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/@polymer/paper-input/-/paper-input-3.2.1.tgz#0fd0d30de3b43ba7d2c8d5d76f870d257b667ebf"
+ integrity sha512-6ghgwQKM6mS0hAQxQqj+tkeEY1VUBqAsrasAm8V5RpNcfSWQC/hhRFxU0beGuKTAhndzezDzWYP6Zz4b8fExGg==
dependencies:
"@polymer/iron-a11y-keys-behavior" "^3.0.0-pre.26"
"@polymer/iron-autogrow-textarea" "^3.0.0-pre.26"
@@ -303,7 +310,14 @@
"@polymer/paper-styles" "^3.0.0-pre.26"
"@polymer/polymer" "^3.0.0"
-"@polymer/polymer@^3.0.0", "@polymer/polymer@^3.0.2", "@polymer/polymer@^3.3.0":
+"@polymer/polymer@^3.0.0", "@polymer/polymer@^3.0.5", "@polymer/polymer@^3.4.1":
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/@polymer/polymer/-/polymer-3.4.1.tgz#333bef25711f8411bb5624fb3eba8212ef8bee96"
+ integrity sha512-KPWnhDZibtqKrUz7enIPOiO4ZQoJNOuLwqrhV2MXzIt3VVnUVJVG5ORz4Z2sgO+UZ+/UZnPD0jqY+jmw/+a9mQ==
+ dependencies:
+ "@webcomponents/shadycss" "^1.9.1"
+
+"@polymer/polymer@^3.0.2":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@polymer/polymer/-/polymer-3.3.1.tgz#9ad48992d2a96775f80b0673f3a615d6df8a3dfc"
integrity sha512-8KaB48tzyMjdsHdxo5KvCAaqmTe7rYDzQAoj/pyEfq9Fp4YfUaS+/xqwYj0GbiDAUNzwkmEQ7dw9cgnRNdKO8A==
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index d03dada..5c9b300 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -168,17 +168,20 @@
writer.Header().Set("Content-Type", "text/html")
} else if isJsFile {
// The following code updates import statements.
- // 1. Keep all imports started with '.' character unchanged (i.e. all relative
- // imports like import ... from './a.js' or import ... from '../b/c/d.js'
- // 2. For other imports it adds '/node_modules/' prefix. Additionally,
- // if an in imported file has .js or .mjs extension, the code keeps
- // the file extension unchanged. Otherwise, it adds .js extension.
+ // 1. if an in imported file has .js or .mjs extension, the code keeps
+ // the file extension unchanged. Otherwise, it adds .js extension
+ // 2. For module imports it adds '/node_modules/' prefix.
// Examples:
// '@polymer/polymer.js' -> '/node_modules/@polymer/polymer.js'
// 'page/page.mjs' -> '/node_modules/page.mjs'
// '@polymer/iron-icon' -> '/node_modules/@polymer/iron-icon.js'
- moduleImportRegexp := regexp.MustCompile("(?m)^(import.*)'([^/.].*?)(\\.(m?)js)?';$")
- data = moduleImportRegexp.ReplaceAll(data, []byte("$1 '/node_modules/$2.${4}js';"))
+ // './element/file' -> './element/file.js'
+ moduleImportRegexp := regexp.MustCompile("(?m)^(import.*)'(.*?)(\\.(m?)js)?';$")
+ data = moduleImportRegexp.ReplaceAll(data, []byte("$1 '$2.${4}js';"))
+
+ moduleImportRegexp = regexp.MustCompile("(?m)^(import.*)'([^/.].*)';$")
+ data = moduleImportRegexp.ReplaceAll(data, []byte("$1 '/node_modules/$2';"))
+
writer.Header().Set("Content-Type", "application/javascript")
} else if strings.HasSuffix(normalizedContentPath, ".css") {
writer.Header().Set("Content-Type", "text/css")