diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..355327f --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[run] +branch = True +omit = appenlight/tests/* diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..d8b23d4 --- /dev/null +++ b/.hgignore @@ -0,0 +1,154 @@ +# Created by .ignore support plugin (hsz.mobi) +syntax: glob + +### Example user template template +### Example user template + +# IntelliJ project files +.idea +*.iml +out +gen### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask instance folder +instance/ + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + + +syntax: regexp +^\.idea$ +syntax: regexp +^\.settings$ +syntax: regexp +^data$ +syntax: regexp +^webassets$ +syntax: regexp +^dist$ +syntax: regexp +^\.project$ +syntax: regexp +^\.pydevproject$ +syntax: regexp +^private$ +syntax: regexp +^appenlight_frontend/build$ +syntax: regexp +^appenlight_frontend/bower_components$ +syntax: regexp +^appenlight_frontend/node_modules$ +^src/node_modules$ +syntax: regexp +^\.pydevproject$ +syntax: regexp +appenlight\.egg-info$ +syntax: regexp +\.pyc$ +syntax: regexp +\celerybeat.* +syntax: regexp +\.iml$ +syntax: regexp +^frontend/build$ +syntax: regexp +^frontend/bower_components$ +syntax: regexp +^frontend/node_modules$ +^frontend/src/node_modules$ +^frontend/build$ + +syntax: regexp +\.db$ + +syntax: regexp +packer_cache + +syntax: regexp +packer/packer + +syntax: regexp +install_appenlight_production.yaml diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..e8a54bc --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,704 @@ +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License, version 3 +(only), as published by the Free Software Foundation. + + +This program incorporates work covered by the following copyright and +permission notice: + + Copyright (c) 2014-2016 - packaging + file: + Copyright (c) 2008-2011 - msgpack-python + file:licenses/msgpack_license.txt + Copyright (c) 2007-2008 - amqp + file:licenses/amqp_license.txt + Copyright (c) 2013 - bcrypt + file:licenses/bcrypt_license.txt + Copyright (c) 2015 - elasticsearch + file:licenses/elasticsearch_license.txt + Copyright (c) 2011-2013 - gevent-websocket + file:licenses/gevent_websocket_license.txt + Copyright (c) 2015 - python-editor + file:licenses/python_editor_license.txt + Copyright (c) 2015 - requests + file:licenses/requests_license.txt + Copyright (c) 2014 - requests-toolbelt + file:licenses/requests_toolbelt_license.txt + +Both 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 +imitations under the License. + + +Below is the full text of GNU Affero General Public License, version 3 + + + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..b0a5c85 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,62 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = appenlight:migrations +version_table = alembic_appenlight_version + +appenlight_config_file = development.ini + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# max length of characters to apply to the +# "slug" field +#truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +sqlalchemy.url = postgresql://test:test@localhost/appenlight + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/CHANGES.txt b/backend/CHANGES.txt new file mode 100644 index 0000000..35a34f3 --- /dev/null +++ b/backend/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/backend/MANIFEST.in b/backend/MANIFEST.in new file mode 100644 index 0000000..da7b313 --- /dev/null +++ b/backend/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst *.md +recursive-include appenlight *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2 *.rst *.otf *.ttf *.svg *.woff *.eot diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..c6d05c8 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,39 @@ +# appenlight README + + +To run the app you need to have meet prerequusites: + +- running elasticsearch (2.2 tested) +- running postgresql 9.5 +- running redis + +# Setup basics + +Set up the basic application database schema: + + appenlight_initialize_db config.ini + +Set up basic elasticsearch schema: + + appenlight-reindex-elasticsearch -c config.ini -t all + + +# Running + +To run the application itself: + + pserve --reload development.ini + +To run celery queue processing: + + celery worker -A appenlight.celery -Q "reports,logs,metrics,default" --ini=development.ini + + +# Testing + +To run test suite: + + py.test appenlight/tests/tests.py --cov appenlight (this looks for testing.ini in repo root) + +WARNING!!! +Some tests will insert data into elasticsearch or redis based on testing.ini diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..f3fd55d --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,49 @@ +repoze.sendmail==4.1 +pyramid==1.7 +pyramid_tm==0.12 +pyramid_debugtoolbar +pyramid_authstack==1.0.1 +SQLAlchemy==1.0.12 +alembic==0.8.6 +webhelpers2==2.0 +transaction==1.4.3 +zope.sqlalchemy==0.7.6 +pyramid_mailer==0.14.1 +redis==2.10.5 +redlock-py==1.0.8 +pyramid_jinja2==2.6.2 +psycopg2==2.6.1 +wtforms==2.1 +celery==3.1.23 +formencode==1.3.0 +psutil==2.1.2 +ziggurat_foundations>=0.6.7 +bcrypt==2.0.0 +appenlight_client +markdown==2.5 +colander==1.2 +defusedxml==0.4.1 +dogpile.cache==0.5.7 +pyramid_redis_sessions==1.0.1 +simplejson==3.8.2 +waitress==0.9.0 +gunicorn==19.4.5 +requests==2.9.1 +requests_oauthlib==0.6.1 +gevent==1.1.1 +gevent-websocket==0.9.5 +pygments==2.1.3 +lxml==3.6.0 +paginate==0.5.4 +paginate-sqlalchemy==0.2.0 +pyelasticsearch==1.4 +six==1.8.0 +mock==1.0.1 +itsdangerous==0.24 +camplight==0.9.6 +jira==0.41 +python-dateutil==2.5.3 +authomatic==0.1.0.post1 +cryptography==1.2.3 +webassets==0.11.1 + diff --git a/backend/setup.py b/backend/setup.py new file mode 100644 index 0000000..c01afef --- /dev/null +++ b/backend/setup.py @@ -0,0 +1,60 @@ +import os +import sys +import re + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +README = open(os.path.join(here, 'README.md')).read() +CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() + +REQUIREMENTS = open(os.path.join(here, 'requirements.txt')).readlines() + +compiled = re.compile('([^=><]*).*') + + +def parse_req(req): + return compiled.search(req).group(1).strip() + + +requires = [_f for _f in map(parse_req, REQUIREMENTS) if _f] + +if sys.version_info[:3] < (2, 5, 0): + requires.append('pysqlite') + +found_packages = find_packages('src') +found_packages.append('appenlight.migrations.versions') +setup(name='appenlight', + version='0.1', + description='appenlight', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pylons", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web wsgi bfg pylons pyramid', + package_dir={'': 'src'}, + packages=found_packages, + include_package_data=True, + zip_safe=False, + test_suite='appenlight', + install_requires=requires, + entry_points={ + 'paste.app_factory': [ + 'main = appenlight:main' + ], + 'console_scripts': [ + 'appenlight-cleanup = appenlight.scripts.cleanup:main', + 'appenlight-initializedb = appenlight.scripts.initialize_db:main', + 'appenlight-migratedb = appenlight.scripts.migratedb:main', + 'appenlight-reindex-elasticsearch = appenlight.scripts.reindex_elasticsearch:main', + 'appenlight-static = appenlight.scripts.static:main', + 'appenlight-make-config = appenlight.scripts.make_config:main', + ] + } + ) diff --git a/backend/src/appenlight/__init__.py b/backend/src/appenlight/__init__.py new file mode 100644 index 0000000..32f7d29 --- /dev/null +++ b/backend/src/appenlight/__init__.py @@ -0,0 +1,285 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import datetime +import logging +import pyelasticsearch +import redis +import os +from pkg_resources import iter_entry_points + +import appenlight.lib.jinja2_filters as jinja2_filters +import appenlight.lib.encryption as encryption + +from authomatic.providers import oauth2, oauth1 +from authomatic import Authomatic +from pyramid.config import Configurator, PHASE3_CONFIG +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy +from pyramid_mailer.mailer import Mailer +from pyramid.renderers import JSON +from pyramid_redis_sessions import session_factory_from_settings +from pyramid.settings import asbool, aslist +from pyramid.security import AllPermissionsList +from pyramid_authstack import AuthenticationStackPolicy +from redlock import Redlock +from sqlalchemy import engine_from_config + +from appenlight.celery import configure_celery +from appenlight.lib import cache_regions +from appenlight.lib.ext_json import json +from appenlight.security import groupfinder, AuthTokenAuthenticationPolicy + +json_renderer = JSON(serializer=json.dumps, indent=4) + +log = logging.getLogger(__name__) + + +def datetime_adapter(obj, request): + return obj.isoformat() + + +def all_permissions_adapter(obj, request): + return '__all_permissions__' + + +json_renderer.add_adapter(datetime.datetime, datetime_adapter) +json_renderer.add_adapter(AllPermissionsList, all_permissions_adapter) + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + auth_tkt_policy = AuthTktAuthenticationPolicy( + settings['authtkt.secret'], + hashalg='sha512', + callback=groupfinder, + max_age=2592000, + secure=asbool(settings.get('authtkt.secure', 'false'))) + auth_token_policy = AuthTokenAuthenticationPolicy( + callback=groupfinder + ) + authorization_policy = ACLAuthorizationPolicy() + authentication_policy = AuthenticationStackPolicy() + authentication_policy.add_policy('auth_tkt', auth_tkt_policy) + authentication_policy.add_policy('auth_token', auth_token_policy) + # set crypto key + encryption.ENCRYPTION_SECRET = settings.get('encryption_secret') + # import this later so encyption key can be monkeypatched + from appenlight.models import DBSession, register_datastores + # update config with cometd info + settings['cometd_servers'] = {'server': settings['cometd.server'], + 'secret': settings['cometd.secret']} + + # Create the Pyramid Configurator. + settings['_mail_url'] = settings['mailing.app_url'] + config = Configurator(settings=settings, + authentication_policy=authentication_policy, + authorization_policy=authorization_policy, + root_factory='appenlight.security.RootFactory', + default_permission='view') + config.set_default_csrf_options(require_csrf=True, header='X-XSRF-TOKEN') + config.add_view_deriver('appenlight.predicates.csrf_view', + name='csrf_view') + + + # later, when config is available + dogpile_config = {'url': settings['redis.url'], + "redis_expiration_time": 86400, + "redis_distributed_lock": True} + cache_regions.regions = cache_regions.CacheRegions(dogpile_config) + config.registry.cache_regions = cache_regions.regions + engine = engine_from_config(settings, 'sqlalchemy.', + json_serializer=json.dumps) + DBSession.configure(bind=engine) + + # json rederer that serializes datetime + config.add_renderer('json', json_renderer) + config.set_request_property('appenlight.lib.request.es_conn', 'es_conn') + config.set_request_property('appenlight.lib.request.get_user', 'user', + reify=True) + config.set_request_property('appenlight.lib.request.get_csrf_token', + 'csrf_token', reify=True) + config.set_request_property('appenlight.lib.request.safe_json_body', + 'safe_json_body', reify=True) + config.set_request_property('appenlight.lib.request.unsafe_json_body', + 'unsafe_json_body', reify=True) + config.add_request_method('appenlight.lib.request.add_flash_to_headers', + 'add_flash_to_headers') + + config.include('pyramid_redis_sessions') + config.include('pyramid_tm') + config.include('pyramid_jinja2') + config.include('appenlight_client.ext.pyramid_tween') + config.include('ziggurat_foundations.ext.pyramid.sign_in') + config.registry.es_conn = pyelasticsearch.ElasticSearch( + settings['elasticsearch.nodes']) + config.registry.redis_conn = redis.StrictRedis.from_url( + settings['redis.url']) + + config.registry.redis_lockmgr = Redlock([settings['redis.redlock.url'], ], + retry_count=0, retry_delay=0) + # mailer + config.registry.mailer = Mailer.from_settings(settings) + + # Configure sessions + session_factory = session_factory_from_settings(settings) + config.set_session_factory(session_factory) + + # Configure renderers and event subscribers + config.add_jinja2_extension('jinja2.ext.loopcontrols') + config.add_jinja2_search_path('appenlight:templates') + # event subscribers + config.add_subscriber("appenlight.subscribers.application_created", + "pyramid.events.ApplicationCreated") + config.add_subscriber("appenlight.subscribers.add_renderer_globals", + "pyramid.events.BeforeRender") + config.add_subscriber('appenlight.subscribers.new_request', + 'pyramid.events.NewRequest') + config.add_view_predicate('context_type_class', + 'appenlight.predicates.contextTypeClass') + + register_datastores(es_conn=config.registry.es_conn, + redis_conn=config.registry.redis_conn, + redis_lockmgr=config.registry.redis_lockmgr) + + # base stuff and scan + + # need to ensure webassets exists otherwise config.override_asset() + # throws exception + if not os.path.exists(settings['webassets.dir']): + os.mkdir(settings['webassets.dir']) + config.add_static_view(path='appenlight:webassets', + name='static', cache_max_age=3600) + config.override_asset(to_override='appenlight:webassets/', + override_with=settings['webassets.dir']) + + config.include('appenlight.views') + config.include('appenlight.views.admin') + config.scan(ignore=['appenlight.migrations', + 'appenlight.scripts', + 'appenlight.tests']) + + # authomatic social auth + authomatic_conf = { + # callback http://yourapp.com/social_auth/twitter + 'twitter': { + 'class_': oauth1.Twitter, + 'consumer_key': settings.get('authomatic.pr.twitter.key', 'X'), + 'consumer_secret': settings.get('authomatic.pr.twitter.secret', + 'X'), + }, + # callback http://yourapp.com/social_auth/facebook + 'facebook': { + 'class_': oauth2.Facebook, + 'consumer_key': settings.get('authomatic.pr.facebook.app_id', 'X'), + 'consumer_secret': settings.get('authomatic.pr.facebook.secret', + 'X'), + 'scope': ['email'], + }, + # callback http://yourapp.com/social_auth/google + 'google': { + 'class_': oauth2.Google, + 'consumer_key': settings.get('authomatic.pr.google.key', 'X'), + 'consumer_secret': settings.get( + 'authomatic.pr.google.secret', 'X'), + 'scope': ['profile', 'email'], + }, + 'github': { + 'class_': oauth2.GitHub, + 'consumer_key': settings.get('authomatic.pr.github.key', 'X'), + 'consumer_secret': settings.get( + 'authomatic.pr.github.secret', 'X'), + 'scope': ['repo', 'public_repo', 'user:email'], + 'access_headers': {'User-Agent': 'AppEnlight'}, + }, + 'bitbucket': { + 'class_': oauth1.Bitbucket, + 'consumer_key': settings.get('authomatic.pr.bitbucket.key', 'X'), + 'consumer_secret': settings.get( + 'authomatic.pr.bitbucket.secret', 'X') + } + } + config.registry.authomatic = Authomatic( + config=authomatic_conf, secret=settings['authomatic.secret']) + + # resource type information + config.registry.resource_types = ['resource', 'application'] + + # plugin information + config.registry.appenlight_plugins = {} + + def register_appenlight_plugin(config, plugin_name, plugin_config): + def register(): + log.warning('Registering plugin: {}'.format(plugin_name)) + if plugin_name not in config.registry.appenlight_plugins: + config.registry.appenlight_plugins[plugin_name] = { + 'javascript': None, + 'static': None, + 'css': None, + 'top_nav': None, + 'celery_tasks': None, + 'celery_beats': None, + 'fulltext_indexer': None, + 'sqlalchemy_migrations': None, + 'default_values_setter': None, + 'resource_types': [], + 'url_gen': None + } + config.registry.appenlight_plugins[plugin_name].update( + plugin_config) + # inform AE what kind of resource types we have available + # so we can avoid failing when a plugin is removed but data + # is still present in the db + if plugin_config.get('resource_types'): + config.registry.resource_types.extend( + plugin_config['resource_types']) + + config.action('appenlight_plugin={}'.format(plugin_name), register) + + config.add_directive('register_appenlight_plugin', + register_appenlight_plugin) + + for entry_point in iter_entry_points(group='appenlight.plugins'): + plugin = entry_point.load() + plugin.includeme(config) + + # include other appenlight plugins explictly if needed + includes = aslist(settings.get('appenlight.includes', [])) + for inc in includes: + config.include(inc) + + # run this after everything registers in configurator + + def pre_commit(): + jinja_env = config.get_jinja2_environment() + jinja_env.filters['tojson'] = json.dumps + jinja_env.filters['toJSONUnsafe'] = jinja2_filters.toJSONUnsafe + + config.action(None, pre_commit, order=PHASE3_CONFIG + 999) + + def wrap_config_celery(): + configure_celery(config.registry) + + config.action(None, wrap_config_celery, order=PHASE3_CONFIG + 999) + + app = config.make_wsgi_app() + return app diff --git a/backend/src/appenlight/celery/__init__.py b/backend/src/appenlight/celery/__init__.py new file mode 100644 index 0000000..04bde68 --- /dev/null +++ b/backend/src/appenlight/celery/__init__.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import logging + +from datetime import timedelta +from celery import Celery +from celery.bin import Option +from celery.schedules import crontab +from celery.signals import worker_init, task_revoked, user_preload_options +from celery.signals import task_prerun, task_retry, task_failure, task_success +from kombu.serialization import register +from pyramid.paster import bootstrap +from pyramid.request import Request +from pyramid.scripting import prepare +from pyramid.settings import asbool +from pyramid.threadlocal import get_current_request + +from appenlight.celery.encoders import json_dumps, json_loads +from appenlight_client.ext.celery import register_signals + +log = logging.getLogger(__name__) + +register('date_json', json_dumps, json_loads, + content_type='application/x-date_json', + content_encoding='utf-8') + +celery = Celery() + +celery.user_options['preload'].add( + Option('--ini', dest='ini', default=None, + help='Specifies pyramid configuration file location.') +) + +@user_preload_options.connect +def on_preload_parsed(options, **kwargs): + """ + This actually configures celery from pyramid config file + """ + celery.conf['INI_PYRAMID'] = options['ini'] + import appenlight_client.client as e_client + ini_location = options['ini'] + if not ini_location: + raise Exception('You need to pass pyramid ini location using ' + '--ini=filename.ini argument to the worker') + env = bootstrap(ini_location) + api_key = env['request'].registry.settings['appenlight.api_key'] + tr_config = env['request'].registry.settings.get( + 'appenlight.transport_config') + CONFIG = e_client.get_config({'appenlight.api_key': api_key}) + if tr_config: + CONFIG['appenlight.transport_config'] = tr_config + APPENLIGHT_CLIENT = e_client.Client(CONFIG) + # log.addHandler(APPENLIGHT_CLIENT.log_handler) + register_signals(APPENLIGHT_CLIENT) + celery.pyramid = env + + +celery_config = { + 'CELERY_IMPORTS': ["appenlight.celery.tasks",], + 'CELERYD_TASK_TIME_LIMIT': 60, + 'CELERYD_MAX_TASKS_PER_CHILD': 1000, + 'CELERY_IGNORE_RESULT': True, + 'CELERY_ACCEPT_CONTENT': ['date_json'], + 'CELERY_TASK_SERIALIZER': 'date_json', + 'CELERY_RESULT_SERIALIZER': 'date_json', + 'BROKER_URL': None, + 'CELERYD_CONCURRENCY': None, + 'CELERY_TIMEZONE': None, + 'CELERYBEAT_SCHEDULE': { + 'alerting': { + 'task': 'appenlight.celery.tasks.alerting', + 'schedule': timedelta(seconds=60) + }, + 'daily_digest': { + 'task': 'appenlight.celery.tasks.daily_digest', + 'schedule': crontab(minute=1, hour='4,12,20') + }, + } +} +celery.config_from_object(celery_config) + +def configure_celery(pyramid_registry): + settings = pyramid_registry.settings + celery_config['BROKER_URL'] = settings['celery.broker_url'] + celery_config['CELERYD_CONCURRENCY'] = settings['celery.concurrency'] + celery_config['CELERY_TIMEZONE'] = settings['celery.timezone'] + if asbool(settings.get('celery.always_eager')): + celery_config['CELERY_ALWAYS_EAGER'] = True + celery_config['CELERY_EAGER_PROPAGATES_EXCEPTIONS'] = True + + for plugin in pyramid_registry.appenlight_plugins.values(): + if plugin.get('celery_tasks'): + celery_config['CELERY_IMPORTS'].extend(plugin['celery_tasks']) + if plugin.get('celery_beats'): + for name, config in plugin['celery_beats']: + celery_config['CELERYBEAT_SCHEDULE'][name] = config + celery.config_from_object(celery_config) + + +@task_prerun.connect +def task_prerun_signal(task_id, task, args, kwargs, **kwaargs): + if hasattr(celery, 'pyramid'): + env = celery.pyramid + env = prepare(registry=env['request'].registry) + proper_base_url = env['request'].registry.settings['mailing.app_url'] + tmp_request = Request.blank('/', base_url=proper_base_url) + # ensure tasks generate url for right domain from config + env['request'].environ['HTTP_HOST'] = tmp_request.environ['HTTP_HOST'] + env['request'].environ['SERVER_PORT'] = tmp_request.environ['SERVER_PORT'] + env['request'].environ['SERVER_NAME'] = tmp_request.environ['SERVER_NAME'] + env['request'].environ['wsgi.url_scheme'] = tmp_request.environ[ + 'wsgi.url_scheme'] + get_current_request().tm.begin() + + +@task_success.connect +def task_success_signal(result, **kwargs): + get_current_request().tm.commit() + if hasattr(celery, 'pyramid'): + celery.pyramid["closer"]() + + +@task_retry.connect +def task_retry_signal(request, reason, einfo, **kwargs): + get_current_request().tm.abort() + if hasattr(celery, 'pyramid'): + celery.pyramid["closer"]() + + +@task_failure.connect +def task_failure_signal(task_id, exception, args, kwargs, traceback, einfo, + **kwaargs): + get_current_request().tm.abort() + if hasattr(celery, 'pyramid'): + celery.pyramid["closer"]() + + +@task_revoked.connect +def task_revoked_signal(request, terminated, signum, expired, **kwaargs): + get_current_request().tm.abort() + if hasattr(celery, 'pyramid'): + celery.pyramid["closer"]() diff --git a/backend/src/appenlight/celery/encoders.py b/backend/src/appenlight/celery/encoders.py new file mode 100644 index 0000000..8cc6baa --- /dev/null +++ b/backend/src/appenlight/celery/encoders.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import json +from datetime import datetime, date, timedelta + +DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.%f' + + +class DateEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, datetime): + return { + '__type__': '__datetime__', + 'iso': obj.strftime(DATE_FORMAT) + } + elif isinstance(obj, date): + return { + '__type__': '__date__', + 'iso': obj.strftime(DATE_FORMAT) + } + elif isinstance(obj, timedelta): + return { + '__type__': '__timedelta__', + 'seconds': obj.total_seconds() + } + else: + return json.JSONEncoder.default(self, obj) + + +def date_decoder(dct): + if '__type__' in dct: + if dct['__type__'] == '__datetime__': + return datetime.strptime(dct['iso'], DATE_FORMAT) + elif dct['__type__'] == '__date__': + return datetime.strptime(dct['iso'], DATE_FORMAT).date() + elif dct['__type__'] == '__timedelta__': + return timedelta(seconds=dct['seconds']) + return dct + + +def json_dumps(obj): + return json.dumps(obj, cls=DateEncoder) + + +def json_loads(obj): + return json.loads(obj.decode('utf8'), object_hook=date_decoder) diff --git a/backend/src/appenlight/celery/tasks.py b/backend/src/appenlight/celery/tasks.py new file mode 100644 index 0000000..3ce5dbe --- /dev/null +++ b/backend/src/appenlight/celery/tasks.py @@ -0,0 +1,610 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import bisect +import collections +import math +from datetime import datetime, timedelta + +import sqlalchemy as sa +import pyelasticsearch + +from celery.utils.log import get_task_logger +from zope.sqlalchemy import mark_changed +from pyramid.threadlocal import get_current_request, get_current_registry +from appenlight.celery import celery +from appenlight.models.report_group import ReportGroup +from appenlight.models import DBSession, Datastores +from appenlight.models.report import Report +from appenlight.models.log import Log +from appenlight.models.request_metric import Metric +from appenlight.models.event import Event + +from appenlight.models.services.application import ApplicationService +from appenlight.models.services.event import EventService +from appenlight.models.services.log import LogService +from appenlight.models.services.report import ReportService +from appenlight.models.services.report_group import ReportGroupService +from appenlight.models.services.user import UserService +from appenlight.models.tag import Tag +from appenlight.lib import print_traceback +from appenlight.lib.utils import parse_proto, in_batches +from appenlight.lib.ext_json import json +from appenlight.lib.redis_keys import REDIS_KEYS +from appenlight.lib.enums import ReportType + +log = get_task_logger(__name__) + +sample_boundries = list(range(100, 10000, 100)) + + +def pick_sample(total_occurences, report_type=1): + every = 1.0 + position = bisect.bisect_left(sample_boundries, total_occurences) + if position > 0: + # 404 + if report_type == 2: + divide = 10.0 + else: + divide = 100.0 + every = sample_boundries[position - 1] / divide + return total_occurences % every == 0 + + +@celery.task(queue="default", default_retry_delay=1, max_retries=2) +def test_exception_task(): + log.error('test celery log', extra={'location': 'celery'}) + log.warning('test celery log', extra={'location': 'celery'}) + raise Exception('Celery exception test') + + +@celery.task(queue="default", default_retry_delay=1, max_retries=2) +def test_retry_exception_task(): + try: + import time + + time.sleep(1.3) + log.error('test retry celery log', extra={'location': 'celery'}) + log.warning('test retry celery log', extra={'location': 'celery'}) + raise Exception('Celery exception test') + except Exception as exc: + test_retry_exception_task.retry(exc=exc) + + +@celery.task(queue="reports", default_retry_delay=600, max_retries=999) +def add_reports(resource_id, params, dataset, environ=None, **kwargs): + proto_version = parse_proto(params.get('protocol_version', '')) + current_time = datetime.utcnow().replace(second=0, microsecond=0) + try: + # we will store solr docs here for single insert + es_report_docs = {} + es_report_group_docs = {} + resource = ApplicationService.by_id(resource_id) + reports = [] + + if proto_version.major < 1 and proto_version.minor < 5: + for report_data in dataset: + report_details = report_data.get('report_details', []) + for i, detail_data in enumerate(report_details): + report_data.update(detail_data) + report_data.pop('report_details') + traceback = report_data.get('traceback') + if traceback is None: + report_data['traceback'] = report_data.get('frameinfo') + # for 0.3 api + error = report_data.pop('error_type', '') + if error: + report_data['error'] = error + if proto_version.minor < 4: + # convert "Unknown" slow reports to + # '' (from older clients) + if (report_data['error'] and + report_data['http_status'] < 500): + report_data['error'] = '' + message = report_data.get('message') + if 'extra' not in report_data: + report_data['extra'] = [] + if message: + report_data['extra'] = [('message', message), ] + reports.append(report_data) + else: + reports = dataset + + tags = [] + es_slow_calls_docs = {} + es_reports_stats_rows = {} + for report_data in reports: + # build report details for later + added_details = 0 + report = Report() + report.set_data(report_data, resource, proto_version) + report._skip_ft_index = True + + report_group = ReportGroupService.by_hash_and_resource( + report.resource_id, + report.grouping_hash + ) + occurences = report_data.get('occurences', 1) + if not report_group: + # total reports will be +1 moment later + report_group = ReportGroup(grouping_hash=report.grouping_hash, + occurences=0, total_reports=0, + last_report=0, + priority=report.priority, + error=report.error, + first_timestamp=report.start_time) + report_group._skip_ft_index = True + report_group.report_type = report.report_type + report.report_group_time = report_group.first_timestamp + add_sample = pick_sample(report_group.occurences, + report_type=report_group.report_type) + if add_sample: + resource.report_groups.append(report_group) + report_group.reports.append(report) + added_details += 1 + DBSession.flush() + if report.partition_id not in es_report_docs: + es_report_docs[report.partition_id] = [] + es_report_docs[report.partition_id].append(report.es_doc()) + tags.extend(list(report.tags.items())) + slow_calls = report.add_slow_calls(report_data, report_group) + DBSession.flush() + for s_call in slow_calls: + if s_call.partition_id not in es_slow_calls_docs: + es_slow_calls_docs[s_call.partition_id] = [] + es_slow_calls_docs[s_call.partition_id].append( + s_call.es_doc()) + # try generating new stat rows if needed + else: + # required for postprocessing to not fail later + report.report_group = report_group + + stat_row = ReportService.generate_stat_rows( + report, resource, report_group) + if stat_row.partition_id not in es_reports_stats_rows: + es_reports_stats_rows[stat_row.partition_id] = [] + es_reports_stats_rows[stat_row.partition_id].append( + stat_row.es_doc()) + + # see if we should mark 10th occurence of report + last_occurences_10 = int(math.floor(report_group.occurences / 10)) + curr_occurences_10 = int(math.floor( + (report_group.occurences + report.occurences) / 10)) + last_occurences_100 = int( + math.floor(report_group.occurences / 100)) + curr_occurences_100 = int(math.floor( + (report_group.occurences + report.occurences) / 100)) + notify_occurences_10 = last_occurences_10 != curr_occurences_10 + notify_occurences_100 = last_occurences_100 != curr_occurences_100 + report_group.occurences = ReportGroup.occurences + occurences + report_group.last_timestamp = report.start_time + report_group.summed_duration = ReportGroup.summed_duration + report.duration + summed_duration = ReportGroup.summed_duration + report.duration + summed_occurences = ReportGroup.occurences + occurences + report_group.average_duration = summed_duration / summed_occurences + report_group.run_postprocessing(report) + if added_details: + report_group.total_reports = ReportGroup.total_reports + 1 + report_group.last_report = report.id + report_group.set_notification_info(notify_10=notify_occurences_10, + notify_100=notify_occurences_100) + DBSession.flush() + report_group.get_report().notify_channel(report_group) + if report_group.partition_id not in es_report_group_docs: + es_report_group_docs[report_group.partition_id] = [] + es_report_group_docs[report_group.partition_id].append( + report_group.es_doc()) + + action = 'REPORT' + log_msg = '%s: %s %s, client: %s, proto: %s' % ( + action, + report_data.get('http_status', 'unknown'), + str(resource), + report_data.get('client'), + proto_version) + log.info(log_msg) + total_reports = len(dataset) + key = REDIS_KEYS['counters']['reports_per_minute'].format(current_time) + Datastores.redis.incr(key, total_reports) + Datastores.redis.expire(key, 3600 * 24) + key = REDIS_KEYS['counters']['reports_per_minute_per_app'].format( + resource_id, current_time) + Datastores.redis.incr(key, total_reports) + Datastores.redis.expire(key, 3600 * 24) + + add_reports_es(es_report_group_docs, es_report_docs) + add_reports_slow_calls_es(es_slow_calls_docs) + add_reports_stats_rows_es(es_reports_stats_rows) + return True + except Exception as exc: + print_traceback(log) + add_reports.retry(exc=exc) + + +@celery.task(queue="es", default_retry_delay=600, max_retries=999) +def add_reports_es(report_group_docs, report_docs): + for k, v in report_group_docs.items(): + Datastores.es.bulk_index(k, 'report_group', v, id_field="_id") + for k, v in report_docs.items(): + Datastores.es.bulk_index(k, 'report', v, id_field="_id", + parent_field='_parent') + + +@celery.task(queue="es", default_retry_delay=600, max_retries=999) +def add_reports_slow_calls_es(es_docs): + for k, v in es_docs.items(): + Datastores.es.bulk_index(k, 'log', v) + + +@celery.task(queue="es", default_retry_delay=600, max_retries=999) +def add_reports_stats_rows_es(es_docs): + for k, v in es_docs.items(): + Datastores.es.bulk_index(k, 'log', v) + + +@celery.task(queue="logs", default_retry_delay=600, max_retries=999) +def add_logs(resource_id, request, dataset, environ=None, **kwargs): + proto_version = request.get('protocol_version') + current_time = datetime.utcnow().replace(second=0, microsecond=0) + + try: + es_docs = collections.defaultdict(list) + application = ApplicationService.by_id(resource_id) + ns_pairs = [] + for entry in dataset: + # gather pk and ns so we can remove older versions of row later + if entry['primary_key'] is not None: + ns_pairs.append({"pk": entry['primary_key'], + "ns": entry['namespace']}) + log_entry = Log() + log_entry.set_data(entry, resource=application) + log_entry._skip_ft_index = True + application.logs.append(log_entry) + DBSession.flush() + # insert non pk rows first + if entry['primary_key'] is None: + es_docs[log_entry.partition_id].append(log_entry.es_doc()) + + # 2nd pass to delete all log entries from db foe same pk/ns pair + if ns_pairs: + ids_to_delete = [] + es_docs = collections.defaultdict(list) + es_docs_to_delete = collections.defaultdict(list) + found_pkey_logs = LogService.query_by_primary_key_and_namespace( + list_of_pairs=ns_pairs) + log_dict = {} + for log_entry in found_pkey_logs: + log_key = (log_entry.primary_key, log_entry.namespace) + if log_key not in log_dict: + log_dict[log_key] = [] + log_dict[log_key].append(log_entry) + + for ns, entry_list in log_dict.items(): + entry_list = sorted(entry_list, key=lambda x: x.timestamp) + # newest row needs to be indexed in es + log_entry = entry_list[-1] + # delete everything from pg and ES, leave the last row in pg + for e in entry_list[:-1]: + ids_to_delete.append(e.log_id) + es_docs_to_delete[e.partition_id].append(e.delete_hash) + + es_docs_to_delete[log_entry.partition_id].append( + log_entry.delete_hash) + + es_docs[log_entry.partition_id].append(log_entry.es_doc()) + + if ids_to_delete: + query = DBSession.query(Log).filter( + Log.log_id.in_(ids_to_delete)) + query.delete(synchronize_session=False) + if es_docs_to_delete: + # batch this to avoid problems with default ES bulk limits + for es_index in es_docs_to_delete.keys(): + for batch in in_batches(es_docs_to_delete[es_index], 20): + query = {'terms': {'delete_hash': batch}} + + try: + Datastores.es.delete_by_query( + es_index, 'log', query) + except pyelasticsearch.ElasticHttpNotFoundError as exc: + log.error(exc) + + total_logs = len(dataset) + + log_msg = 'LOG_NEW: %s, entries: %s, proto:%s' % ( + str(application), + total_logs, + proto_version) + log.info(log_msg) + # mark_changed(session) + key = REDIS_KEYS['counters']['logs_per_minute'].format(current_time) + Datastores.redis.incr(key, total_logs) + Datastores.redis.expire(key, 3600 * 24) + key = REDIS_KEYS['counters']['logs_per_minute_per_app'].format( + resource_id, current_time) + Datastores.redis.incr(key, total_logs) + Datastores.redis.expire(key, 3600 * 24) + add_logs_es(es_docs) + return True + except Exception as exc: + print_traceback(log) + add_logs.retry(exc=exc) + + +@celery.task(queue="es", default_retry_delay=600, max_retries=999) +def add_logs_es(es_docs): + for k, v in es_docs.items(): + Datastores.es.bulk_index(k, 'log', v) + + +@celery.task(queue="metrics", default_retry_delay=600, max_retries=999) +def add_metrics(resource_id, request, dataset, proto_version): + current_time = datetime.utcnow().replace(second=0, microsecond=0) + try: + application = ApplicationService.by_id_cached()(resource_id) + application = DBSession.merge(application, load=False) + es_docs = [] + rows = [] + for metric in dataset: + tags = dict(metric['tags']) + server_n = tags.get('server_name', metric['server_name']).lower() + tags['server_name'] = server_n or 'unknown' + new_metric = Metric( + timestamp=metric['timestamp'], + resource_id=application.resource_id, + namespace=metric['namespace'], + tags=tags) + rows.append(new_metric) + es_docs.append(new_metric.es_doc()) + session = DBSession() + session.bulk_save_objects(rows) + session.flush() + + action = 'METRICS' + metrics_msg = '%s: %s, metrics: %s, proto:%s' % ( + action, + str(application), + len(dataset), + proto_version + ) + log.info(metrics_msg) + + mark_changed(session) + key = REDIS_KEYS['counters']['metrics_per_minute'].format(current_time) + Datastores.redis.incr(key, len(rows)) + Datastores.redis.expire(key, 3600 * 24) + key = REDIS_KEYS['counters']['metrics_per_minute_per_app'].format( + resource_id, current_time) + Datastores.redis.incr(key, len(rows)) + Datastores.redis.expire(key, 3600 * 24) + add_metrics_es(es_docs) + return True + except Exception as exc: + print_traceback(log) + add_metrics.retry(exc=exc) + + +@celery.task(queue="es", default_retry_delay=600, max_retries=999) +def add_metrics_es(es_docs): + for doc in es_docs: + partition = 'rcae_m_%s' % doc['timestamp'].strftime('%Y_%m_%d') + Datastores.es.index(partition, 'log', doc) + + +@celery.task(queue="default", default_retry_delay=5, max_retries=2) +def check_user_report_notifications(resource_id, since_when=None): + try: + request = get_current_request() + application = ApplicationService.by_id(resource_id) + if not application: + return + error_key = REDIS_KEYS['reports_to_notify_per_type_per_app'].format( + ReportType.error, resource_id) + slow_key = REDIS_KEYS['reports_to_notify_per_type_per_app'].format( + ReportType.slow, resource_id) + error_group_ids = Datastores.redis.smembers(error_key) + slow_group_ids = Datastores.redis.smembers(slow_key) + Datastores.redis.delete(error_key) + Datastores.redis.delete(slow_key) + err_gids = [int(g_id) for g_id in error_group_ids] + slow_gids = [int(g_id) for g_id in list(slow_group_ids)] + group_ids = err_gids + slow_gids + occurence_dict = {} + for g_id in group_ids: + key = REDIS_KEYS['counters']['report_group_occurences'].format( + g_id) + val = Datastores.redis.get(key) + Datastores.redis.delete(key) + if val: + occurence_dict[g_id] = int(val) + else: + occurence_dict[g_id] = 1 + report_groups = ReportGroupService.by_ids(group_ids) + report_groups.options(sa.orm.joinedload(ReportGroup.last_report_ref)) + + ApplicationService.check_for_groups_alert( + application, 'alert', report_groups=report_groups, + occurence_dict=occurence_dict, since_when=since_when) + users = set([p.user for p in application.users_for_perm('view')]) + report_groups = report_groups.all() + for user in users: + UserService.report_notify(user, request, application, + report_groups=report_groups, + occurence_dict=occurence_dict, + since_when=since_when) + for group in report_groups: + # marks report_groups as notified + if not group.notified: + group.notified = True + except Exception as exc: + print_traceback(log) + raise + + +@celery.task(queue="default", default_retry_delay=1, max_retries=2) +def close_alerts(since_when=None): + log.warning('Checking alerts') + try: + event_types = [Event.types['error_report_alert'], + Event.types['slow_report_alert'], ] + statuses = [Event.statuses['active']] + # get events older than 5 min + events = EventService.by_type_and_status( + event_types, + statuses, + older_than=(since_when - timedelta(minutes=5))) + for event in events: + # see if we can close them + event.validate_or_close( + since_when=(since_when - timedelta(minutes=1))) + except Exception as exc: + print_traceback(log) + raise + + +@celery.task(queue="default", default_retry_delay=600, max_retries=999) +def update_tag_counter(tag_name, tag_value, count): + try: + query = DBSession.query(Tag).filter(Tag.name == tag_name).filter( + sa.cast(Tag.value, sa.types.TEXT) == sa.cast(json.dumps(tag_value), + sa.types.TEXT)) + query.update({'times_seen': Tag.times_seen + count, + 'last_timestamp': datetime.utcnow()}, + synchronize_session=False) + session = DBSession() + mark_changed(session) + return True + except Exception as exc: + print_traceback(log) + update_tag_counter.retry(exc=exc) + + +@celery.task(queue="default") +def update_tag_counters(): + """ + Sets task to update counters for application tags + """ + tags = Datastores.redis.lrange(REDIS_KEYS['seen_tag_list'], 0, -1) + Datastores.redis.delete(REDIS_KEYS['seen_tag_list']) + c = collections.Counter(tags) + for t_json, count in c.items(): + tag_info = json.loads(t_json) + update_tag_counter.delay(tag_info[0], tag_info[1], count) + + +@celery.task(queue="default") +def daily_digest(): + """ + Sends daily digest with top 50 error reports + """ + request = get_current_request() + apps = Datastores.redis.smembers(REDIS_KEYS['apps_that_had_reports']) + Datastores.redis.delete(REDIS_KEYS['apps_that_had_reports']) + since_when = datetime.utcnow() - timedelta(hours=8) + log.warning('Generating daily digests') + for resource_id in apps: + resource_id = resource_id.decode('utf8') + end_date = datetime.utcnow().replace(microsecond=0, second=0) + filter_settings = {'resource': [resource_id], + 'tags': [{'name': 'type', + 'value': ['error'], 'op': None}], + 'type': 'error', 'start_date': since_when, + 'end_date': end_date} + + reports = ReportGroupService.get_trending( + request, filter_settings=filter_settings, limit=50) + + application = ApplicationService.by_id(resource_id) + if application: + users = set([p.user for p in application.users_for_perm('view')]) + for user in users: + user.send_digest(request, application, reports=reports, + since_when=since_when) + + +@celery.task(queue="default") +def alerting(): + """ + Loop that checks redis for info and then issues new tasks to celery to + perform the following: + - which applications should have new alerts opened + - which currently opened alerts should be closed + """ + start_time = datetime.utcnow() + # transactions are needed for mailer + apps = Datastores.redis.smembers(REDIS_KEYS['apps_that_had_reports']) + Datastores.redis.delete(REDIS_KEYS['apps_that_had_reports']) + for app in apps: + log.warning('Notify for app: %s' % app) + check_user_report_notifications.delay(app.decode('utf8')) + # clear app ids from set + close_alerts.delay(since_when=start_time) + + +@celery.task(queue="default", soft_time_limit=3600 * 4, hard_time_limit=3600 * 4, + max_retries=999) +def logs_cleanup(resource_id, filter_settings): + request = get_current_request() + request.tm.begin() + es_query = { + "_source": False, + "size": 5000, + "query": { + "filtered": { + "filter": { + "and": [{"term": {"resource_id": resource_id}}] + } + } + } + } + + query = DBSession.query(Log).filter(Log.resource_id == resource_id) + if filter_settings['namespace']: + query = query.filter(Log.namespace == filter_settings['namespace'][0]) + es_query['query']['filtered']['filter']['and'].append( + {"term": {"namespace": filter_settings['namespace'][0]}} + ) + query.delete(synchronize_session=False) + request.tm.commit() + result = request.es_conn.search(es_query, index='rcae_l_*', + doc_type='log', es_scroll='1m', + es_search_type='scan') + scroll_id = result['_scroll_id'] + while True: + log.warning('log_cleanup, app:{} ns:{} batch'.format( + resource_id, + filter_settings['namespace'] + )) + es_docs_to_delete = [] + result = request.es_conn.send_request( + 'POST', ['_search', 'scroll'], + body=scroll_id, query_params={"scroll": '1m'}) + scroll_id = result['_scroll_id'] + if not result['hits']['hits']: + break + for doc in result['hits']['hits']: + es_docs_to_delete.append({"id": doc['_id'], + "index": doc['_index']}) + + for batch in in_batches(es_docs_to_delete, 10): + Datastores.es.bulk([Datastores.es.delete_op(doc_type='log', + **to_del) + for to_del in batch]) diff --git a/backend/src/appenlight/fil.py b/backend/src/appenlight/fil.py new file mode 100644 index 0000000..d2dce6a --- /dev/null +++ b/backend/src/appenlight/fil.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +def filter_callable(structure, section=None): + structure['SOMEVAL'] = '***REMOVED***' + return structure diff --git a/backend/src/appenlight/forms.py b/backend/src/appenlight/forms.py new file mode 100644 index 0000000..22a09b8 --- /dev/null +++ b/backend/src/appenlight/forms.py @@ -0,0 +1,903 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import wtforms +import formencode +import re +import pyramid.threadlocal +import datetime +import appenlight.lib.helpers as h + +from appenlight.models.user import User +from appenlight.models.group import Group +from appenlight.models import DBSession +from appenlight.models.alert_channel import AlertChannel +from appenlight.models.integrations import IntegrationException +from appenlight.models.integrations.campfire import CampfireIntegration +from appenlight.models.integrations.bitbucket import BitbucketIntegration +from appenlight.models.integrations.github import GithubIntegration +from appenlight.models.integrations.flowdock import FlowdockIntegration +from appenlight.models.integrations.hipchat import HipchatIntegration +from appenlight.models.integrations.jira import JiraClient +from appenlight.models.integrations.slack import SlackIntegration +from appenlight.lib.ext_json import json +from wtforms.ext.csrf.form import SecureForm +from wtforms.compat import iteritems +from collections import defaultdict + +_ = str + +strip_filter = lambda x: x.strip() if x else None +uppercase_filter = lambda x: x.upper() if x else None + +FALSE_VALUES = ('false', '', False, None) + + +class CSRFException(Exception): + pass + + +class ReactorForm(SecureForm): + def __init__(self, formdata=None, obj=None, prefix='', csrf_context=None, + **kwargs): + super(ReactorForm, self).__init__(formdata=formdata, obj=obj, + prefix=prefix, + csrf_context=csrf_context, **kwargs) + self._csrf_context = csrf_context + + def generate_csrf_token(self, csrf_context): + return csrf_context.session.get_csrf_token() + + def validate_csrf_token(self, field): + request = self._csrf_context or pyramid.threadlocal.get_current_request() + is_from_auth_token = 'auth:auth_token' in request.effective_principals + if is_from_auth_token: + return True + + if field.data != field.current_token: + # try to save the day by using token from angular + if request.headers.get('X-XSRF-TOKEN') != field.current_token: + raise CSRFException('Invalid CSRF token') + + @property + def errors_dict(self): + r_dict = defaultdict(list) + for k, errors in self.errors.items(): + r_dict[k].extend([str(e) for e in errors]) + return r_dict + + @property + def errors_json(self): + return json.dumps(self.errors_dict) + + def populate_obj(self, obj, ignore_none=False): + """ + Populates the attributes of the passed `obj` with data from the form's + fields. + + :note: This is a destructive operation; Any attribute with the same name + as a field will be overridden. Use with caution. + """ + if ignore_none: + for name, field in iteritems(self._fields): + if field.data is not None: + field.populate_obj(obj, name) + else: + for name, field in iteritems(self._fields): + field.populate_obj(obj, name) + + css_classes = {} + ignore_labels = {} + + +class SignInForm(ReactorForm): + came_from = wtforms.HiddenField() + sign_in_user_name = wtforms.StringField(_('User Name')) + sign_in_user_password = wtforms.PasswordField(_('Password')) + + ignore_labels = ['submit'] + css_classes = {'submit': 'btn btn-primary'} + + html_attrs = {'sign_in_user_name': {'placeholder': 'Your login'}, + 'sign_in_user_password': { + 'placeholder': 'Your password'}} + + +from wtforms.widgets import html_params, HTMLString + + +def select_multi_checkbox(field, ul_class='set', **kwargs): + """Render a multi-checkbox widget""" + kwargs.setdefault('type', 'checkbox') + field_id = kwargs.pop('id', field.id) + html = ['
    ' % html_params(id=field_id, class_=ul_class)] + for value, label, checked in field.iter_choices(): + choice_id = '%s-%s' % (field_id, value) + options = dict(kwargs, name=field.name, value=value, id=choice_id) + if checked: + options['checked'] = 'checked' + html.append('
  • ' % html_params(**options)) + html.append('
  • ' % (choice_id, label)) + html.append('
') + return HTMLString(''.join(html)) + + +def button_widget(field, button_cls='ButtonField btn btn-default', **kwargs): + """Render a button widget""" + kwargs.setdefault('type', 'button') + field_id = kwargs.pop('id', field.id) + kwargs.setdefault('value', field.label.text) + html = ['' % (html_params(id=field_id, + class_=button_cls), + kwargs['value'],)] + return HTMLString(''.join(html)) + + +def clean_whitespace(value): + if value: + return value.strip() + return value + + +def found_username_validator(form, field): + user = User.by_user_name(field.data) + # sets user to recover in email validator + form.field_user = user + if not user: + raise wtforms.ValidationError('This username does not exist') + + +def found_username_email_validator(form, field): + user = User.by_email(field.data) + if not user: + raise wtforms.ValidationError('Email is incorrect') + + +def unique_username_validator(form, field): + user = User.by_user_name(field.data) + if user: + raise wtforms.ValidationError('This username already exists in system') + + +def unique_groupname_validator(form, field): + group = Group.by_group_name(field.data) + mod_group = getattr(form, '_modified_group', None) + if group and (not mod_group or mod_group.id != group.id): + raise wtforms.ValidationError( + 'This group name already exists in system') + + +def unique_email_validator(form, field): + user = User.by_email(field.data) + if user: + raise wtforms.ValidationError('This email already exists in system') + + +def email_validator(form, field): + validator = formencode.validators.Email() + try: + validator.to_python(field.data) + except formencode.Invalid as e: + raise wtforms.ValidationError(e) + + +def unique_alert_email_validator(form, field): + q = DBSession.query(AlertChannel) + q = q.filter(AlertChannel.channel_name == 'email') + q = q.filter(AlertChannel.channel_value == field.data) + email = q.first() + if email: + raise wtforms.ValidationError( + 'This email already exists in alert system') + + +def blocked_email_validator(form, field): + blocked_emails = [ + 'goood-mail.org', + 'shoeonlineblog.com', + 'louboutinemart.com', + 'guccibagshere.com', + 'nikeshoesoutletforsale.com' + ] + data = field.data or '' + domain = data.split('@')[-1] + if domain in blocked_emails: + raise wtforms.ValidationError('Don\'t spam') + + +def old_password_validator(form, field): + if not field.user.check_password(field.data or ''): + raise wtforms.ValidationError('You need to enter correct password') + + +class UserRegisterForm(ReactorForm): + user_name = wtforms.StringField( + _('User Name'), + filters=[strip_filter], + validators=[ + wtforms.validators.Length(min=2, max=30), + wtforms.validators.Regexp( + re.compile(r'^[\.\w-]+$', re.UNICODE), + message="Invalid characters used"), + unique_username_validator, + wtforms.validators.DataRequired() + ]) + + user_password = wtforms.PasswordField(_('User Password'), + filters=[strip_filter], + validators=[ + wtforms.validators.Length(min=4), + wtforms.validators.DataRequired() + ]) + + email = wtforms.StringField(_('Email Address'), + filters=[strip_filter], + validators=[email_validator, + unique_email_validator, + blocked_email_validator, + wtforms.validators.DataRequired()], + description=_("We promise we will not share " + "your email with anyone")) + first_name = wtforms.HiddenField(_('First Name')) + last_name = wtforms.HiddenField(_('Last Name')) + + ignore_labels = ['submit'] + css_classes = {'submit': 'btn btn-primary'} + + html_attrs = {'user_name': {'placeholder': 'Your login'}, + 'user_password': {'placeholder': 'Your password'}, + 'email': {'placeholder': 'Your email'}} + + +class UserCreateForm(UserRegisterForm): + status = wtforms.BooleanField('User status', + false_values=FALSE_VALUES) + + +class UserUpdateForm(UserCreateForm): + user_name = None + user_password = wtforms.PasswordField(_('User Password'), + filters=[strip_filter], + validators=[ + wtforms.validators.Length(min=4), + wtforms.validators.Optional() + ]) + email = wtforms.StringField(_('Email Address'), + filters=[strip_filter], + validators=[email_validator, + wtforms.validators.DataRequired()]) + + +class LostPasswordForm(ReactorForm): + email = wtforms.StringField(_('Email Address'), + filters=[strip_filter], + validators=[email_validator, + found_username_email_validator, + wtforms.validators.DataRequired()]) + + submit = wtforms.SubmitField(_('Reset password')) + ignore_labels = ['submit'] + css_classes = {'submit': 'btn btn-primary'} + + +class ChangePasswordForm(ReactorForm): + old_password = wtforms.PasswordField( + 'Old Password', + filters=[strip_filter], + validators=[old_password_validator, + wtforms.validators.DataRequired()]) + + new_password = wtforms.PasswordField( + 'New Password', + filters=[strip_filter], + validators=[wtforms.validators.Length(min=4), + wtforms.validators.DataRequired()]) + new_password_confirm = wtforms.PasswordField( + 'Confirm Password', + filters=[strip_filter], + validators=[wtforms.validators.EqualTo('new_password'), + wtforms.validators.DataRequired()]) + submit = wtforms.SubmitField('Change Password') + ignore_labels = ['submit'] + css_classes = {'submit': 'btn btn-primary'} + + +class CheckPasswordForm(ReactorForm): + password = wtforms.PasswordField( + 'Password', + filters=[strip_filter], + validators=[old_password_validator, + wtforms.validators.DataRequired()]) + + +class NewPasswordForm(ReactorForm): + new_password = wtforms.PasswordField( + 'New Password', + filters=[strip_filter], + validators=[wtforms.validators.Length(min=4), + wtforms.validators.DataRequired()]) + new_password_confirm = wtforms.PasswordField( + 'Confirm Password', + filters=[strip_filter], + validators=[wtforms.validators.EqualTo('new_password'), + wtforms.validators.DataRequired()]) + submit = wtforms.SubmitField('Set Password') + ignore_labels = ['submit'] + css_classes = {'submit': 'btn btn-primary'} + + +class CORSTextAreaField(wtforms.StringField): + """ + This field represents an HTML ``\n" + + " \n" + + " \n" + + " \n" + + "\n" + + "

Functional

\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "

Global Rate Limiting

\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " Save configuration\n" + + " \n" + + "\n" + + "\n" + + "\n" + + "\n" + + "
\n" + + "
\n" + + "

Plugin Configuration

\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/admin/configs/parent_view.html', + "
" + ); + + + $templateCache.put('templates/admin/groups/groups_create.html', + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "
\n" + + "

Permissions summary

\n" + + "
\n" + + "
\n" + + "

Direct application permissions

\n" + + "\n" + + "
    \n" + + "
  • \n" + + " {{ perm.self.resource_name }}\n" + + "\n" + + "
    \n" + + "\n" + + " {{ perm.self.owner ? 'Resource owner' : perm_name }}\n" + + "\n" + + " \n" + + " \n" + + " \n" + + "
    \n" + + "
  • \n" + + "
\n" + + "\n" + + "

Direct dashboard permissions

\n" + + "\n" + + "
    \n" + + "
  • \n" + + " {{ perm.self.resource_name }}\n" + + "\n" + + "
    \n" + + " {{ perm.self.owner ? 'Resource owner' : perm_name }}\n" + + "\n" + + " \n" + + " \n" + + " \n" + + "
    \n" + + "
  • \n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "
\n" + + "

User list

\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
UsernameEmailStatusFirst NameLast NameLast login
{{user.user_name}}{{user.email}}{{user.first_name}}{{user.last_name}}{{user.last_login_date | isoToRelativeTime}}\n" + + " \n" + + " \n" + + " \n" + + "
    \n" + + "
  • No
  • \n" + + "
  • Yes
  • \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + ); + + + $templateCache.put('templates/admin/groups/groups_list.html', + "\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
Group nameDescriptionMember count
{{group.group_name}}{{group.description}}{{group.member_count}}\n" + + " \n" + + " \n" + + " \n" + + "
    \n" + + "
  • No
  • \n" + + "
  • Yes
  • \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + ); + + + $templateCache.put('templates/admin/groups/parent_view.html', + "
" + ); + + + $templateCache.put('templates/admin/parent_view.html', + "
\n" + + "
\n" + + "
Users and groups
\n" + + " \n" + + "
\n" + + "
\n" + + "
Resources
\n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + "
System
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + ); + + + $templateCache.put('templates/admin/partitions.html', + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " DELETE Daily Partitions\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + " \n" + + " Check All\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
DateIndices
{{row[0]}}\n" + + "
    \n" + + "
  • \n" + + " ES: {{partition.name}}\n" + + "
  • \n" + + "
  • \n" + + " PG: {{partition.name}}\n" + + "
  • \n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " DELETE Permanent Partitions\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + " Check All\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
DateIndices
{{row[0]}}\n" + + "
    \n" + + "
  • \n" + + " ES: {{partition.name}}\n" + + "
  • \n" + + "
  • \n" + + " PG: {{partition.name}}\n" + + "
  • \n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + ); + + + $templateCache.put('templates/admin/system.html', + "\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "

\n" + + " System Info\n" + + "

\n" + + "
\n" + + "
\n" + + "\n" + + "

System Load:\n" + + " 1min: {{system.systemLoad[0]}}, 5min: {{system.systemLoad[1]}}, 15min: {{system.systemLoad[2]}}\n" + + "

\n" + + "

Awaiting tasks:\n" + + "

    \n" + + "
  • reports: {{system.queueStats.waiting_reports}}
  • \n" + + "
  • logs: {{system.queueStats.waiting_logs}}
  • \n" + + "
  • metrics: {{system.queueStats.waiting_metrics}}
  • \n" + + "
  • other: {{system.queueStats.waiting_other}}
  • \n" + + "
\n" + + "

\n" + + "

Queue stats from last minute:\n" + + "

    \n" + + "
  • Processed reports: {{system.queueStats.processed_reports}}
  • \n" + + "
  • Processed logs: {{system.queueStats.processed_logs}}
  • \n" + + "
  • Processed metrics: {{system.queueStats.processed_metrics}}
  • \n" + + "
\n" + + "

\n" + + "\n" + + "

Disks:\n" + + "

    \n" + + "
  • \n" + + " {{disk.device}} {{disk.free}}/{{disk.total}}, {{disk.percentage}}% used\n" + + "
  • \n" + + "
\n" + + "

\n" + + "\n" + + "

Process stats:\n" + + "

    \n" + + "
  • FD soft limits: {{system.selfInfo.fds.soft}}
  • \n" + + "
  • FD hard limits: {{system.selfInfo.fds.hard}}
  • \n" + + "
  • Memlock soft limits: {{system.selfInfo.memlock.soft}}
  • \n" + + "
  • Memlock hard limits: {{system.selfInfo.memlock.hard}}
  • \n" + + "
\n" + + "

\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " Postgresql Tables\n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
Table nameSize
{{row.table_name}}{{row.size_human}}
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + " \n" + + " Elasticsearch Indices\n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
Index nameSize
{{row.name}}{{row.size_human}}
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + " \n" + + " Processes\n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
OwnerPIDCPUMEMName
{{row.owner}}{{row.pid}}{{row.cpu}}{{row.mem_usage}} ({{row.mem_percentage}}%){{row.name}}
{{row.command}}
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + " \n" + + " Python packages\n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
{{package.name}}{{package.version}}
\n" + + "

\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/admin/users/parent_view.html', + "
" + ); + + + $templateCache.put('templates/admin/users/users_create.html', + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + " \n" + + " Re-login to user\n" + + "
    \n" + + "
  • No
  • \n" + + "
  • Yes
  • \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "\n" + + "

Generate password\n" + + " 0\">(generated password: {{user.gen_pass}})\n" + + "

\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "
\n" + + "

Permission Summary

\n" + + "
\n" + + "
\n" + + "

Direct application permissions

\n" + + "\n" + + "
    \n" + + "
  • \n" + + " {{ perm.self.resource_name }}\n" + + "
    \n" + + "\n" + + " {{ perm.self.owner ? 'Resource owner' : perm_name }}\n" + + "\n" + + " \n" + + " \n" + + " \n" + + "
    \n" + + "
  • \n" + + "
\n" + + "\n" + + "

Direct dashboard permissions

\n" + + "\n" + + "
    \n" + + "
  • \n" + + " {{ perm.self.resource_name }}\n" + + "
    \n" + + "\n" + + " {{ perm.self.owner ? 'Resource owner' : perm_name }}\n" + + "\n" + + " \n" + + " \n" + + " \n" + + "
    \n" + + "
  • \n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + ); + + + $templateCache.put('templates/admin/users/users_list.html', + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + " {{users.activeUsers}} active out of {{users.users.length}} users\n" + + "
\n" + + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
UsernameEmailStatusFirst NameLast NameLast login
{{user.user_name}}{{user.email}}{{user.first_name}}{{user.last_name}}{{user.last_login_date | isoToRelativeTime}}\n" + + " \n" + + " \n" + + " \n" + + "
    \n" + + "
  • No
  • \n" + + "
  • Yes
  • \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/applications/applications_purge_logs.html', + "\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/applications/applications_update.html', + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " API keys\n" + + " \n" + + "\n" + + "

PRIVATE API KEY:

\n" + + "

\n" + + "

{{ application.resource.api_key }}
\n" + + "

\n" + + "

PUBLIC API KEY (for javascript clients):

\n" + + "

\n" + + "

{{ application.resource.public_key }}
\n" + + "

\n" + + "

Your key will be used to identify to which application your data\n" + + " belongs to please keep them private at all times.

\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + " \n" + + " Regenerate API keys\n" + + " \n" + + "

Are you sure you want to regenerate API KEY for this application?

\n" + + "

All client application keys will need to be updated.

\n" + + "
\n" + + " \n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "

How to connect your application?

\n" + + "

Visit our developer documentation for step-by-step integration instructions.

\n" + + "
\n" + + "

\n" + + " \"Django\n" + + " \"Pyramid\n" + + " \"Flask\n" + + "\n" + + " \"Javascript\n" + + " \"Node.js\"\n" + + " \"Ruby\n" + + " \"PHP\n" + + " \n" + + "\n" + + "

\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "

Required for Javascript error tracking (one line one domain, skip http:// part)

\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "

Application requires to send at least this amount of error reports per minute to open alert

\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "\n" + + "
\n" + + " \n" + + "

Application requires to send at least this amount of slow reports per minute to open alert

\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "

Allow permanent storage of logs in separate DB partitions (only administrator can enable this feature)

\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "

Plugins

\n" + + "
\n" + + "
\n" + + "\n" + + " \n" + + " \n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "

API Testing

\n" + + "
\n" + + "
\n" + + "

Please be sure to add at least one email alert channel for your account.

\n" + + "

This will enable App Enlight to send you notification emails about errors inside your application.

\n" + + "

After this is done you can use this CURL commands to test APIs:

\n" + + "

(Please note that the data like execution times is semi randomly generated)

\n" + + " \n" + + " \n" + + " \n" + + " Log API\n" + + " \n" + + "\n" + + "
\n" + + "
\n" +
+    "curl -H \"Content-Type: application/json\" -k {{AeConfig.urls.baseUrl}}api/logs?protocol_version=0.5\\&api_key={{application.resource.api_key}} -d '\n" +
+    "    [\n" +
+    "      {\n" +
+    "      \"log_level\": \"WARNING\",\n" +
+    "      \"message\": \"OMG ValueError happened\",\n" +
+    "      \"namespace\": \"some.namespace.indicator\",\n" +
+    "      \"request_id\": \"SOME_UUID\",\n" +
+    "      \"permanent\": false,\n" +
+    "      \"primary_key\": \"random_key\",\n" +
+    "      \"server\": \"some.server.hostname\",\n" +
+    "      \"date\": \"{{application.momentJs.utc().milliseconds(0).toISOString()}}\",\n" +
+    "      \"tags\": [[\"tag1\",\"value\"], [\"tag2\", 5]]\n" +
+    "      },\n" +
+    "      {\n" +
+    "      \"log_level\": \"ERROR\",\n" +
+    "      \"message\": \"OMG ValueError happened2\",\n" +
+    "      \"namespace\": \"some.namespace.indicator\",\n" +
+    "      \"request_id\": \"SOME_UUID\",\n" +
+    "      \"permanent\": false,\n" +
+    "      \"server\": \"some.server.hostname\",\n" +
+    "      \"date\": \"{{application.momentJs.utc().milliseconds(0).toISOString()}}\"\n" +
+    "      }\n" +
+    "    ]'\n" +
+    "                    
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + " \n" + + " Report API\n" + + " \n" + + "\n" + + "
\n" + + "
\n" +
+    "curl -H \"Content-Type: application/json\" -k {{AeConfig.urls.baseUrl}}api/reports?protocol_version=0.5\\&api_key={{application.resource.api_key}} -d '\n" +
+    "    [{\n" +
+    "    \"client\": \"your-client-name-python\",\n" +
+    "    \"language\": \"python\",\n" +
+    "    \"view_name\": \"views/foo:bar\",\n" +
+    "    \"server\": \"SERVERNAME/INSTANCENAME\",\n" +
+    "    \"priority\": 5,\n" +
+    "    \"error\": \"OMG ValueError happened\",\n" +
+    "    \"occurences\":1,\n" +
+    "    \"http_status\": 500,\n" +
+    "    \"tags\": [[\"tag1\",\"value\"], [\"tag2\", 5]],\n" +
+    "    \"username\": \"USER\",\n" +
+    "    \"url\": \"HTTP://SOMEURL\",\n" +
+    "    \"ip\": \"127.0.0.1\",\n" +
+    "    \"start_time\": \"{{application.momentJs.utc().milliseconds(0).toISOString()}}\",\n" +
+    "    \"end_time\": \"{{application.momentJs.utc().milliseconds(0).add(2, 'seconds').toISOString()}}\",\n" +
+    "    \"user_agent\": \"BROWSER_AGENT\",\n" +
+    "    \"extra\": [[\"message\",\"CUSTOM MESSAGE\"], [\"custom_value\", \"some payload\"]],\n" +
+    "    \"request_id\": \"SOME_UUID\",\n" +
+    "    \"request\": {\"REQUEST_METHOD\": \"GET\",\n" +
+    "             \"PATH_INFO\": \"/FOO/BAR\",\n" +
+    "             \"POST\": {\"FOO\":\"BAZ\",\"XXX\":\"YYY\"}\n" +
+    "             },\n" +
+    "    \"slow_calls\":[{\n" +
+    "                   \"start\": \"{{application.momentJs.utc().milliseconds(0).toISOString()}}\",\n" +
+    "                   \"end\": \"{{application.momentJs.utc().milliseconds(0).add(1, 'seconds').toISOString()}}\",\n" +
+    "                   \"type\": \"sql\",\n" +
+    "                   \"subtype\": \"postgresql\",\n" +
+    "                   \"parameters\": [\"QPARAM1\",\"QPARAM2\",\"QPARAMX\"],\n" +
+    "                   \"statement\": \"QUERY\"\n" +
+    "                   }],\n" +
+    "    \"request_stats\": {\n" +
+    "                    \"main\": 2.50779,\n" +
+    "                    \"nosql\": 0.01008,\n" +
+    "                    \"nosql_calls\": 17.0,\n" +
+    "                    \"remote\": 0.0,\n" +
+    "                    \"remote_calls\": 0.0,\n" +
+    "                    \"sql\": 1,\n" +
+    "                    \"sql_calls\": 1.0,\n" +
+    "                    \"tmpl\": 0.0,\n" +
+    "                    \"tmpl_calls\": 0.0,\n" +
+    "                    \"custom\": 0.0,\n" +
+    "                    \"custom_calls\": 0.0\n" +
+    "                },\n" +
+    "    \"traceback\": [\n" +
+    "                {\"cline\": \"return foo_bar_baz(1,2,3)\",\n" +
+    "                \"file\": \"somedir/somefile.py\",\n" +
+    "                \"fn\": \"somefunction\",\n" +
+    "                \"line\": 454,\n" +
+    "                \"vars\": [[\"a_list\",\n" +
+    "                         [\"1\",2,\"4\",\"5\",6]],\n" +
+    "                         [\"b\", {\"1\": \"2\", \"ccc\": \"ddd\", \"1\": \"a\"}],\n" +
+    "                         [\"obj\", \"object object at 0x7f0030853dc0\"]]\n" +
+    "                        },\n" +
+    "                        {\"cline\": \"OMG ValueError happened\",\n" +
+    "                        \"file\": \"\",\n" +
+    "                        \"fn\": \"\",\n" +
+    "                        \"line\": \"\",\n" +
+    "                        \"vars\": []}\n" +
+    "                        ]\n" +
+    "                        }]'\n" +
+    "                    
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + "\n" + + " \n" + + " Metrics API\n" + + " \n" + + "\n" + + "
\n" + + "
\n" +
+    "curl -H \"Content-Type: application/json\" -k {{AeConfig.urls.baseUrl}}api/general_metrics?protocol_version=0.5\\&api_key={{application.resource.api_key}} -d '\n" +
+    "        [{\n" +
+    "        \"namespace\": \"some.monitor\",\n" +
+    "        \"timestamp\": \"{{application.momentJs.utc().milliseconds(0).toISOString()}}\",\n" +
+    "        \"server_name\": \"server.name\",\n" +
+    "        \"tags\": [[\"value1\", 15.7], [\"value2\", 26]]}]'\n" +
+    "                    
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + "\n" + + " \n" + + " Request Stats API\n" + + " \n" + + "\n" + + "
\n" + + "
\n" +
+    "curl -H \"Content-Type: application/json\" -k {{AeConfig.urls.baseUrl}}api/request_stats?protocol_version=0.5\\&api_key={{application.resource.api_key}} -d '\n" +
+    "        [{\"server\": \"some.server.hostname\",\n" +
+    "          \"timestamp\": \"{{application.momentJs.utc().milliseconds(0).toISOString()}}\",\n" +
+    "          \"metrics\": [[\"dir/module:func\",\n" +
+    "               {\"custom\": 0.0,\n" +
+    "                \"custom_calls\": 0,\n" +
+    "                \"main\": 0.01664,\n" +
+    "                \"nosql\": 0.00061,\n" +
+    "                \"nosql_calls\": 23,\n" +
+    "                \"remote\": 0.0,\n" +
+    "                \"remote_calls\": 0,\n" +
+    "                \"requests\": 1,\n" +
+    "                \"sql\": 0.00105,\n" +
+    "                \"sql_calls\": 2,\n" +
+    "                \"tmpl\": 0.0,\n" +
+    "                \"tmpl_calls\": 0}],\n" +
+    "              [\"SomeView.function\",\n" +
+    "               {\"custom\": 0.0,\n" +
+    "                \"custom_calls\": 0,\n" +
+    "                \"main\": 0.647261,\n" +
+    "                \"nosql\": 0.306554,\n" +
+    "                \"nosql_calls\": 140,\n" +
+    "                \"remote\": 0.0,\n" +
+    "                \"remote_calls\": 0,\n" +
+    "                \"requests\": 28,\n" +
+    "                \"sql\": 0.0,\n" +
+    "                \"sql_calls\": 0,\n" +
+    "                \"tmpl\": 0.0,\n" +
+    "                \"tmpl_calls\": 0}]]\n" +
+    "                }]'\n" +
+    "                    
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + " \n" + + "\n" + + "
\n" + + "
\n" + + "

Postprocessing

\n" + + "
\n" + + "
\n" + + "

This section allows you influence the rating of report groups - if rule is matched once its not executed anymore

\n" + + "\n" + + "

\n" + + " Add rule\n" + + "

\n" + + "\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "

Administration

\n" + + "
\n" + + "
\n" + + "

Transfer ownership

\n" + + "

Please note that by transfering ownership you WILL lose access to the application data and new owner needs to give you access permission

\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "

Remove application

\n" + + "

This operation will wipe out all data from database - there is no undo.

\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/applications/breadcrumbs.html', + "
    \n" + + "
  1. Applications
  2. \n" + + "
  3. Owned applications
  4. \n" + + "
  5. Modify application
  6. \n" + + "
  7. Integrations
  8. \n" + + "
  9. Log Purging
  10. \n" + + "
\n" + ); + + + $templateCache.put('templates/applications/integrations.html', + "\n" + + "\n" + + "\n" + + " \n" + + "\n" + ); + + + $templateCache.put('templates/applications/integrations/bitbucket.html', + "\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "

Bitbucket Integration

\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + " \n" + + "\n" + + "
\n" + + "\n" + + " \n" + + " \n" + + "\n" + + "
\n" + + "
https://bitbucket.org/
\n" + + " \n" + + "
/
\n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + " \n" + + "\n" + + "
\n" + + " \n" + + " \n" + + " Remove Integration\n" + + "
    \n" + + "
  • No
  • \n" + + "
  • Yes
  • \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "

Remember you first need to\n" + + " \n" + + " authorize your user account\n" + + " with Bitbucket before we can send issues on your behalf.

\n" + + "\n" + + "

Every user will have to authorize App Enlight to access Bitbucket to be able to post issues.

\n" + + "\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/applications/integrations/campfire.html', + "\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "

Campfire Integration

\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + "
\n" + + " \n" + + "\n" + + "
\n" + + "
http://
\n" + + " \n" + + "
.campfirenow.com
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + " \n" + + " \n" + + "

\n" + + " Room ID list separated by comma\n" + + "

\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "\n" + + " \n" + + " Remove Integration\n" + + "
    \n" + + "
  • No
  • \n" + + "
  • Yes
  • \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/applications/integrations/flowdock.html', + "\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "

Flowdock Integration

\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + "\n" + + "
\n" + + "\n" + + " \n" + + "\n" + + " \n" + + " Remove Integration\n" + + "
    \n" + + "
  • No
  • \n" + + "
  • Yes
  • \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/applications/integrations/github.html', + "\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "

Github Integration

\n" + + "\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + "\n" + + "
\n" + + "\n" + + " \n" + + " \n" + + "\n" + + "
\n" + + "
https://api.github.com/
\n" + + " \n" + + "
/
\n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + "\n" + + " \n" + + "\n" + + " \n" + + " Remove Integration\n" + + "
    \n" + + "
  • No
  • \n" + + "
  • Yes
  • \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "

Remember you first need to\n" + + " \n" + + " authorize your user account\n" + + " with Github before we can send issues on your behalf.

\n" + + "\n" + + "

Every user will have to authorize App Enlight to access Github to be able to post issues.

\n" + + "\n" + + "
\n" + + "
Private repository access
\n" + + "
\n" + + "

If you need access to private repositories profile page allows you to require token including private repository permissions.

\n" + + "\n" + + "

Registration page OAuth does NOT give you token with private repository access permissions.

\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/applications/integrations/hipchat.html', + "\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "

Hipchat Integration

\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "\n" + + "

\n" + + " Room ID list separated by comma\n" + + "

\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " Remove Integration\n" + + "
    \n" + + "
  • No
  • \n" + + "
  • Yes
  • \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/applications/integrations/jira.html', + "\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "

Jira Integration

\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + " \n" + + "
\n" + + "\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + " \n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + " \n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + " \n" + + "\n" + + " \n" + + " Remove Integration\n" + + "
    \n" + + "
  • No
  • \n" + + "
  • Yes
  • \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/applications/integrations/slack.html', + "\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "

Slack Integration

\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + "
\n" + + " \n" + + "\n" + + " \n" + + " Remove Integration\n" + + "
    \n" + + "
  • No
  • \n" + + "
  • Yes
  • \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/applications/integrations/webhooks.html', + "\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "

Webhooks Integration

\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + " \n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + " \n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " Remove Integration\n" + + "
    \n" + + "
  • No
  • \n" + + "
  • Yes
  • \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/applications/list.html', + "\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "

You have to create a new application first.

\n" + + "\n" + + "
\n" + + "\n" + + " 0\">\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
Resource NameDomainsOptions
{{application.resource_name}}{{application.domains}}\n" + + " Update\n" + + " Integrations\n" + + "
\n" + + "\n" + + "
\n" + ); + + + $templateCache.put('templates/applications/parent_view.html', + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + ); + + + $templateCache.put('templates/dashboard.html', + "\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "\n" + + " \n" + + "\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "

\n" + + " \n" + + "

\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "\n" + + "

\n" + + " Average requests per second from all servers\n" + + "

\n" + + "\n" + + "

\n" + + " Average response time from all servers\n" + + "

\n" + + "\n" + + "

\n" + + " Aggregated average time spent per request - broken to layers\n" + + "

\n" + + "\n" + + "

\n" + + " Aggregated reports sent by your application\n" + + "

\n" + + "\n" + + "

\n" + + " Aggregated slow reports sent by your application\n" + + "

\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + "\n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
ServerApdex\n" + + " \n" + + " rpmavg. response
\n" + + " {{ server.name }}\n" + + " \n" + + " {{ server.apdex }} %\n" + + " \n" + + " {{ server.rpm }}rpm\n" + + " \n" + + " {{ server.avg_response_time }}s\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "

Newest errors (real-time)\n" + + "

\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "

No new reports

\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "

Request breakdown over {{ index.timeSpan.label }}

\n" + + "
\n" + + "
\n" + + "

\n" + + " \n" + + "

\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " {{view.view_name}}\n" + + " {{view.view_name}}\n" + + "\n" + + "
\n" + + " \n" + + " avg. response {{view.avg_response}}s in\n" + + " {{view.requests|numberToThousands}} requests\n" + + "\n" + + " \n" + + "    Latest reports:\n" + + " {{$index+1}}\n" + + " \n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "

\n" + + " Report groups trending over {{ index.timeSpan.label }}\n" + + "

\n" + + "
\n" + + "
\n" + + "

\n" + + " \n" + + "

\n" + + "\n" + + "

\n" + + " No reports found\n" + + "

\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "
\n" + + "

\n" + + " Most common slow calls over {{ index.timeSpan.label }}\n" + + "

\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " {{call.occurences|numberToThousands}}\n" + + " \n" + + " {{call.statement}}\n" + + "
\n" + + " {{call.statement_type}}\n" + + " {{call.statement_subtype}}\n" + + " {{call.total_duration/call.occurences|round:2}}s\n" + + " \n" + + " Latest reports:\n" + + " {{$index+1}} \n" + + " \n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/directives/permissions.html', + "
\n" + + "
\n" + + "

Permissions

\n" + + "
\n" + + "
\n" + + "

Here you can set permissions for others to access your app data.

\n" + + "\n" + + "

For example you can let other staff member view or alter error reports.

\n" + + "\n" + + "
0\">\n" + + "

Group permissions

\n" + + "\n" + + "
    \n" + + "
  • \n" + + " {{ perm.self.group_name }}\n" + + "
    \n" + + " Resource owner\n" + + " \n" + + " {{ perm_name }}\n" + + "
      \n" + + "
    • No
    • \n" + + "
    • Yes
    • \n" + + "
    \n" + + "
    \n" + + "
    \n" + + "
  • \n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + " \n" + + " {{ permission }}\n" + + " \n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "

User permissions

\n" + + "
\n" + + "
    \n" + + "
  • \n" + + " {{ perm.self.user_name }}\n" + + "
    \n" + + " Resource owner\n" + + " \n" + + " {{ perm_name }}\n" + + "
      \n" + + "
    • No
    • \n" + + "
    • Yes
    • \n" + + "
    \n" + + "
    \n" + + "
    \n" + + "
  • \n" + + "
\n" + + "
\n" + + "
\n" + + "

First enter username or full email of person you want to give access to (the person needs to be already registered in App Enlight)

\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + " \n" + + " {{ permission }}\n" + + " \n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/directives/plugin_config.html', + "
\n" + + "
Plugin: {{tmpl.name}}
\n" + + " \n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/directives/postprocess_action.html', + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
    \n" + + "
  • No
  • \n" + + "
  • Yes
  • \n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "\n" + + "  Save changes\n" + + "\n" + + "
\n" + + "
\n" + + "

Meeting following criteria:

\n" + + " \n" + + " {{ctrl.rule}}\n" + + " \n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/directives/report_alert_action.html', + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
    \n" + + "
  • No
  • \n" + + "
  • Yes
  • \n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "\n" + + "  Save changes\n" + + "\n" + + "
\n" + + "
\n" + + "

Channels:

\n" + + "
    \n" + + "
  • \n" + + " {{channel.channel_visible_value}}\n" + + "
    \n" + + " \n" + + " \n" + + "
      \n" + + "
    • No
    • \n" + + "
    • Yes
    • \n" + + "
    \n" + + "
    \n" + + "
    \n" + + "
  • \n" + + "
\n" + + "
\n" + + " \n" + + " Add Channel\n" + + "
\n" + + "
\n" + + " You need to create an alert channel before you can assign it to your rule.\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "

Meeting following criteria:

\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/directives/rule_read_only.html', + "
\n" + + "\n" + + " \n" + + " {{rule_ctrlr.readOnlyPossibleFields[rule_ctrlr.rule.field]}}\n" + + " \n" + + "\n" + + " \n" + + " is {{rule_ctrlr.ruleDefinitions.allOps[rule_ctrlr.rule.op]}} {{rule_ctrlr.rule.value}}\n" + + " \n" + + "\n" + + " \n" + + "

Subrules

\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/directives/rule.html', + "
\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + "\n" + + " \n" + + "\n" + + "
\n" + + "\n" + + " \n" + + "

Subrules

\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + " Add rule\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
    \n" + + "
  • No
  • \n" + + "
  • Yes
  • \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/directives/search_type_ahead.html', + "\n" + + " {{match.model.tag}}\n" + + " {{match.label}}\n" + + " - {{match.model.example}}\n" + + "
{{match.model.description}}
\n" + + "\n" + + "
\n" + ); + + + $templateCache.put('templates/directives/user_search_type_ahead.html', + "\n" + + " {{match.label}} -\n" + + " {{match.model.name}}\n" + + "\n" + ); + + + $templateCache.put('templates/events.html', + "
\n" + + "
\n" + + "\n" + + "

Event history

\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "

For {{ event.resource_name }}

\n" + + "\n" + + "

{{ event.text }}

\n" + + " created:\n" + + " \n" + + " \n" + + " | closed:\n" + + " \n" + + " \n" + + "
\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/integrations/bitbucket.html', + "
\n" + + "

Add issue to Bitbucket

\n" + + "
\n" + + "
\n" + + "
{{msg}}
\n" + + "
\n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + ); + + + $templateCache.put('templates/integrations/github.html', + "
\n" + + "

Add issue to Github

\n" + + "
\n" + + "
\n" + + "
{{msg}}
\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + ); + + + $templateCache.put('templates/integrations/jira.html', + "
\n" + + "

Add issue to Jira

\n" + + "
\n" + + "
\n" + + "
{{msg}}
\n" + + "
\n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + ); + + + $templateCache.put('templates/loader.html', + "
\n" + + " \n" + + "
\n" + ); + + + $templateCache.put('templates/logs.html', + "\n" + + "\n" + + "
\n" + + "\n" + + "

\n" + + " Search params:\n" + + " \n" + + " {{tag.type}}\n" + + " {{ tag.type == 'resource' ? logs.applications[tag.value].resource_name : tag.value }}\n" + + "\n" + + " \n" + + " \n" + + "

\n" + + "\n" + + "

\n" + + "\n" + + " \n" + + "\n" + + "

\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "\n" + + "

\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + " \n" + + "
Logs
ApplicationMessageWhen
\n" + + " \n" + + " {{log.resource_name}}\n" + + " \n" + + " \n" + + " level: {{log.log_level}}\n" + + " \n" + + " namespace: {{log.namespace}}\n" + + " \n" + + " {{tag}}: {{value}}\n" + + "
\n" + + " {{log.message}}\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "\n" + + "
\n" + ); + + + $templateCache.put('templates/quickstart.html', + "

App Enlight quickstart

\n" + + "\n" + + "

\n" + + " 1\n" + + " For App Enlight to operate you need to\n" + + " create app profile that allows\n" + + " you to\n" + + " obtain API key that one of the clients can use.\n" + + "

\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "

\n" + + " 2\n" + + " It is a good idea to configure an\n" + + " \n" + + " email alert channel that you can use to receive\n" + + " notifications about events that happen in your application.\n" + + "

\n" + + "\n" + + "

\n" + + " It can be the same email account you used to register withing App Enlight -\n" + + " although we often recommend using separate errors@... account\n" + + " designated for alert notifications.\n" + + "

\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "

\n" + + " 3\n" + + " In order for your application to stream meaningful information you will need to\n" + + " integrate a compatible client for your language of choice.\n" + + "

\n" + + "\n" + + "

Head over to \n" + + " developers section for information on currently available\n" + + " clients that you can plug into your software

\n" + ); + + + $templateCache.put('templates/register.html', + "" + ); + + + $templateCache.put('templates/reports/list_slow.html', + "\n" + + "\n" + + "
\n" + + "\n" + + "

\n" + + " Search params:\n" + + " \n" + + " {{tag.type}}\n" + + " {{ tag.type == 'resource' ? reports_list.applications[tag.value].resource_name : tag.value }}\n" + + "\n" + + " \n" + + " \n" + + "

\n" + + "\n" + + "

\n" + + "\n" + + "

\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "\n" + + "

\n" + + "\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + " \n" + + "
Slow Request Reports
#Avg. durationApplicationWhen Location
\n" + + " {{report.group.priority}}\n" + + " \n" + + " {{report.group.occurences|numberToThousands}}\n" + + " \n" + + " {{report.group.average_duration.toFixed(3)}}s\n" + + "
{{report.resource_name}}
\n" + + " @{{report.tags.server_name}}
\n" + + " \n" + + " \n" + + " {{report.group.last_timestamp.replace('T', ' ').slice(0,16)}}\n" + + " \n" + + " {{ report.tags.view_name || report.url_path}}
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "\n" + + "
\n" + ); + + + $templateCache.put('templates/reports/list.html', + "\n" + + "\n" + + "
\n" + + "\n" + + "

\n" + + " Search params:\n" + + " \n" + + " {{tag.type}}\n" + + " {{ tag.type == 'resource' ? reports_list.applications[tag.value].resource_name : tag.value }}\n" + + "\n" + + " \n" + + " \n" + + "

\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "\n" + + "

\n" + + "\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + " \n" + + "
Reports
#ApplicationWhen Error
\n" + + " {{report.group.priority}}\n" + + " \n" + + " {{report.group.occurences|numberToThousands}}\n" + + " \n" + + " \n" + + "
{{report.resource_name}}
\n" + + " @{{report.tags.server_name}}
\n" + + " \n" + + " \n" + + " {{report.group.last_timestamp.replace('T', ' ').slice(0,16)}}\n" + + " {{report.error || 'Unknown Exception'}}
\n" + + " {{ report.tags.view_name || report.url_path}}
\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "\n" + + "
\n" + ); + + + $templateCache.put('templates/reports/parent_view.html', + "
" + ); + + + $templateCache.put('templates/reports/small_report_group_list.html', + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
{{ report_group.occurences|numberToThousands }}\n" + + " \n" + + " {{ report_group.error || \"Slow Report\"}}\n" + + "
\n" + + " {{report_group.summed_duration/report_group.occurences|round:2}}s\n" + + " {{ report_group.view_name || report_group.url_path}}\n" + + "
\n" + + " @{{applications[report_group.resource_id].resource_name}}
\n" + + " {{report_group.last_timestamp | isoToRelativeTime}}\n" + + "
\n" + ); + + + $templateCache.put('templates/reports/small_report_list.html', + "\n" + + " 0\" class=\"animate-repeat\">\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
{{ report.group.occurences|numberToThousands }}\n" + + " \n" + + " {{ report.error || \"Slow Report\"}}\n" + + "
\n" + + " {{report.group.summed_duration/report.group.occurences|round:2}}s\n" + + " {{ report.view_name || report.url_path}}\n" + + "
\n" + + " @{{applications[report.resource_id].resource_name}}
\n" + + " {{report.last_timestamp | isoToRelativeTime}}\n" + + "
\n" + ); + + + $templateCache.put('templates/reports/view.html', + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "
\n" + + " OOPS something went wrong :(\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + " 2\">\n" + + " Go back\n" + + " Assign report\n" + + " to user\n" + + "\n" + + " \n" + + " Mark fixed\n" + + "\n" + + " \n" + + " \n" + + " Integrations\n" + + " \n" + + " \n" + + " \n" + + "\n" + + " Make {{report.group.public ? 'private' : 'public'}}\n" + + "\n" + + "\n" + + " Delete\n" + + "
    \n" + + "
  • No
  • \n" + + "
  • Yes
  • \n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "

Report Information

\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 0\">\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
Occurences{{report.report.group.occurences}}
HTTP status{{report.report.http_status}}
Priority{{report.report.group.priority}}
Public URL\n" + + "
\n" + + " \n" + + "
\n" + + "
URL{{report.report.url}}
Remote IP{{report.report.ip}}
User Agent{{report.report.user_agent}}
Message{{report.report.message}}
Duration\n" + + " {{report.report.duration}}s\n" + + "
First occured\n" + + " \n" + + "
Last occured\n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + "

Performance stats

\n" + + "\n" + + "
\n" + + " 0 || stat.value > 0\">\n" + + " {{stat.calls}}\n" + + " {{stat.name}} calls\n" + + " \n" + + " Other\n" + + " \n" + + " \n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + " 0s\n" + + "
\n" + + "
\n" + + " {{report.report.duration.toFixed(3)}}s\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "

Tags

\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
Username/UIDView NameServer Name{{ tag }}\n" + + " {{ value }}
\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "

Report history

\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + " Prev. detail\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + " {{report.report.start_time.replace('T', ' ')}} UTC\n" + + " ID: {{report.report.request_id}}\n" + + " \n" + + "
\n" + + "
\n" + + " \n" + + " Next detail \n" + + "
\n" + + "
\n" + + "\n" + + "

{{report.report.error}}

\n" + + "\n" + + "
\n" + + "\n" + + "

Traceback

\n" + + "\n" + + " \n" + + "\n" + + "
\n" + + "
{{report.rawTraceback}}
\n" + + "
\n" + + "
\n" + + "\n" + + "
= report.traceback.length-10 || report.traceback.length <= 10 || report.showLong\">\n" + + "
\n" + + " \n" + + " \n" + + " \n" + + "\n" + + " \n" + + "\n" + + " \n" + + " File {{frame.file || 'Unknown file'}},\n" + + " \n" + + " \n" + + " Module {{frame.module || 'Unknown module'}},\n" + + " \n" + + " line {{frame.line || 'Unknown line'}}\n" + + "\n" + + " in {{frame.fn || 'Unknown function'}}\n" + + "\n" + + "
\n" + + "
{{frame.cline || 'Unknown context'}}
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
{{ fvar[0] }}\n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " Slow Calls\n" + + " \n" + + "\n" + + "

Slow Calls

\n" + + "\n" + + "
0\">\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " No slow calls reported\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "\n" + + " \n" + + " \n" + + " Request details\n" + + " \n" + + "\n" + + "

Extra

\n" + + "
\n" + + "

Request details

\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + " \n" + + " Logs\n" + + " \n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "

No logs found

\n" + + "\n" + + " 0\">\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + " \n" + + "
Logs
MessageWhen
\n" + + " \n" + + " level: {{log.log_level}}\n" + + " \n" + + " namespace: {{log.namespace}}\n" + + " \n" + + " {{tag}}: {{value}}\n" + + "
\n" + + " {{log.message}}\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "\n" + + " \n" + + " \n" + + " Comments\n" + + " {{report.report.comments.length}}\n" + + "\n" + + " \n" + + "\n" + + "

Comments

\n" + + "\n" + + "

No comments yet - be first to add one!

\n" + + "\n" + + "
\n" + + "

\n" + + " {{comment.user_name}}\n" + + " \n" + + "

\n" + + "

{{comment.body}}

\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + " \n" + + " \n" + + " Affected users\n" + + " {{report.report.affected_users_count}}\n" + + "\n" + + " \n" + + "\n" + + "

50 most affected users ID's by this issue:

\n" + + "
    \n" + + "
  • \n" + + " {{user.username}} {{user.count}}\n" + + "
  • \n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/user/alert_channels_email.html', + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "

Adding email alert channel - after you authorize your email in the system we can send alerts directly to this mailbox.

\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/user/alert_channels_list.html', + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "

Report alert rules

\n" + + "

\n" + + " Add top-level rule\n" + + "

\n" + + "\n" + + " \n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "

Alert channels

\n" + + "\n" + + "

Here you can configure your alert channels.

\n" + + "\n" + + "

An alert channel serves as means of delivery of notifications about important events that happen in your applications.

\n" + + "\n" + + "
You can add more integrations that support different alert channels via application management panel.
\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
{{ channel.channel_visible_value }}\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Alerts\n" + + " \n" + + " \n" + + " Daily digests\n" + + " \n" + + "\n" + + " \n" + + " Remove\n" + + "
    \n" + + "
  • No
  • \n" + + "
  • Yes
  • \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + ); + + + $templateCache.put('templates/user/alert_channels.html', + "" + ); + + + $templateCache.put('templates/user/auth_tokens.html', + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "
You can use those tokens to authenticate yourself when performing various API calls
\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
Your current tokens
DescriptionCreatedExpires

{{token.description}}

\n" + + "
{{token.token| limitTo:token.limit}}...
\n" + + "
{{token.creation_date | isoToRelativeTime}}{{token.expires | isoToRelativeTime}}\n" + + " Never\n" + + " \n" + + " \n" + + "
    \n" + + "
  • No
  • \n" + + "
  • Yes
  • \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + ); + + + $templateCache.put('templates/user/breadcrumbs.html', + "
    \n" + + "
  1. Settings
  2. \n" + + "
  3. User Profile
  4. \n" + + "
  5. Password
  6. \n" + + "
  7. Identities
  8. \n" + + "
\n" + + "\n" + + "
    \n" + + "
  1. Notifications
  2. \n" + + "
  3. Alert Channels
  4. \n" + + "
  5. Create email channel
  6. \n" + + "
\n" + ); + + + $templateCache.put('templates/user/index.html', + "" + ); + + + $templateCache.put('templates/user/menu.html', + "
\n" + + "
Applications
\n" + + " \n" + + "
\n" + + "\n" + + "\n" + + "
\n" + + "
Settings
\n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + "
Notifications
\n" + + " \n" + + "
" + ); + + + $templateCache.put('templates/user/parent_view.html', + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/user/profile_edit.html', + "\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/user/profile_identities.html', + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "

No external providers linked yet

\n" + + "
    \n" + + "
  • \n" + + "
    \n" + + " \n" + + " \n" + + "
      \n" + + "
    • No
    • \n" + + "
    • Yes
    • \n" + + "
    \n" + + "
    \n" + + "
    \n" + + " @{{ provider.provider }}: {{ provider.id }}\n" + + "
  • \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/user/profile_password.html', + "\n" + + "\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + ); + + + $templateCache.put('templates/user/profile.html', + "" + ); + +}]); + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +var aeconfig = angular.module('appenlight.config', []); +aeconfig.factory('AeConfig', function () { + var obj = {}; + obj.flashMessages = decodeEncodedJSON(window.AE.flash_messages); + obj.timeOptions = decodeEncodedJSON(window.AE.timeOptions); + obj.plugins = decodeEncodedJSON(window.AE.plugins); + obj.ws_url = window.AE.ws_url; + obj.urls = window.AE.urls; + + // set keys on values because we wont be able to retrieve them everywhere + for (var key in obj.timeOptions) { + obj.timeOptions[key]['key'] = key; + } + console.info('config', obj); + return obj; +}); + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers').controller('AdminApplicationsListController', AdminApplicationsListController); + +AdminApplicationsListController.$inject = ['applicationsResource']; + +function AdminApplicationsListController(applicationsResource) { + + var vm = this; + vm.loading = {applications: true}; + + vm.applications = applicationsResource.query({ + root_list: true, + resource_type: 'application' + }, function (data) { + vm.loading = {applications: false}; + }); +}; + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers').controller('ConfigsListController', ConfigsListController); + +ConfigsListController.$inject = ['configsResource', 'configsNoIdResource']; + +function ConfigsListController(configsResource, configsNoIdResource) { + var vm = this; + vm.loading = {config: true}; + + var filters = [ + 'template_footer_html:global', + 'list_groups_to_non_admins:global', + 'per_application_reports_rate_limit:global', + 'per_application_logs_rate_limit:global', + 'per_application_metrics_rate_limit:global', + ]; + + vm.configs = {}; + + vm.configList = configsResource.query({filter: filters}, + function (data) { + vm.loading = {config: false}; + _.each(data, function (item) { + if (vm.configs[item.section] === undefined) { + vm.configs[item.section] = {}; + } + vm.configs[item.section][item.key] = item; + }); + }); + + vm.save = function () { + vm.loading.config = true; + _.each(vm.configList, function (item) { + item.$save(); + }); + vm.loading.config = false; + }; + +}; + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers').controller('AdminGroupsCreateController', AdminGroupsCreateController); + +AdminGroupsCreateController.$inject = ['$state', 'groupsResource', 'groupsPropertyResource', 'sectionViewResource', 'AeConfig']; + +function AdminGroupsCreateController($state, groupsResource, groupsPropertyResource, sectionViewResource, AeConfig) { + + var vm = this; + vm.loading = { + group: false, + resource_permissions: false, + users: false + }; + + vm.form = { + autocompleteUser: '', + } + + + if (typeof $state.params.groupId !== 'undefined') { + vm.loading.group = true; + var groupId = $state.params.groupId; + vm.group = groupsResource.get({groupId: groupId}, function (data) { + vm.loading.group = false; + }); + + vm.resource_permissions = groupsPropertyResource.query( + {groupId: groupId, key: 'resource_permissions'}, function (data) { + vm.loading.resource_permissions = false; + var tmpObj = { + 'group': { + 'application': {}, + 'dashboard': {} + } + }; + _.each(data, function (item) { + + var section = tmpObj[item.type][item.resource_type]; + if (typeof section[item.resource_id] == 'undefined') { + section[item.resource_id] = { + self: item, + permissions: [] + } + } + section[item.resource_id].permissions.push(item.perm_name); + + }); + vm.resourcePermissions = tmpObj; + }); + + vm.users = groupsPropertyResource.query( + {groupId: groupId, key: 'users'}, function (data) { + vm.loading.users = false; + }, function () { + vm.loading.users = false; + }); + + } + else { + var groupId = null; + } + + var formResponse = function (response) { + if (response.status === 422) { + setServerValidation(vm.groupForm, response.data); + } + vm.loading.group = false; + }; + + vm.createGroup = function () { + vm.loading.group = true; + if (groupId) { + groupsResource.update({groupId: vm.group.id}, vm.group, function (data) { + setServerValidation(vm.groupForm); + vm.loading.group = false; + }, formResponse); + } + else { + groupsResource.save(vm.group, function (data) { + $state.go('admin.group.update', {groupId: data.id}); + }, formResponse); + } + }; + + vm.removeUser = function (user) { + groupsPropertyResource.delete( + {groupId: groupId, key: 'users', user_name: user.user_name}, + function (data) { + vm.loading.users = false; + vm.users = _.filter(vm.users, function (item) { + return item != user; + }); + }, function () { + vm.loading.users = false; + }); + }; + + vm.addUser = function () { + groupsPropertyResource.save( + {groupId: groupId, key: 'users'}, + {user_name: vm.form.autocompleteUser}, + function (data) { + vm.loading.users = false; + vm.users.push(data); + vm.form.autocompleteUser = ''; + }, function () { + vm.loading.users = false; + }); + } + + vm.searchUsers = function (searchPhrase) { + + return sectionViewResource.query({ + section: 'users_section', + view: 'search_users', + 'user_name': searchPhrase + }).$promise.then(function (data) { + return _.map(data, function (item) { + return item.user; + }); + }); + } +}; + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers').controller('AdminGroupsController', AdminGroupsController); + +AdminGroupsController.$inject = ['groupsResource']; + +function AdminGroupsController(groupsResource) { + + var vm = this; + vm.loading = {groups: true}; + + vm.groups = groupsResource.query({}, function (data) { + vm.loading = {groups: false}; + vm.activeUsers = _.reduce(vm.groups, function(memo, val){ + if (val.status == 1){ + return memo + 1; + } + return memo; + }, 0); + + }); + + + vm.removeGroup = function (group) { + groupsResource.remove({groupId: group.id}, function (data, responseHeaders) { + + if (data) { + var index = vm.groups.indexOf(group); + if (index !== -1) { + vm.groups.splice(index, 1); + vm.activeGroups -= 1; + } + } + }); + } +}; + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers').controller('AdminPartitionsController', AdminPartitionsController); + +AdminPartitionsController.$inject = ['sectionViewResource']; + +function AdminPartitionsController(sectionViewResource) { + var vm = this; + vm.permanentPartitions = []; + vm.dailyPartitions = []; + vm.loading = {partitions: true}; + vm.dailyChecked = false; + vm.permChecked = false; + vm.dailyConfirm = ''; + vm.permConfirm = ''; + + + vm.loadPartitions = function (data) { + var permanentPartitions = vm.transformPartitionList( + data.permanent_partitions); + var dailyPartitions = vm.transformPartitionList( + data.daily_partitions); + vm.permanentPartitions = permanentPartitions; + vm.dailyPartitions = dailyPartitions; + vm.loading = {partitions: false}; + }; + + vm.setCheckedList = function (scope) { + var toTest = null; + if (scope === 'dailyPartitions'){ + toTest = 'dailyChecked'; + } + else{ + toTest = 'permChecked'; + } + + if (vm[toTest]) { + var val = true; + } + else { + var val = false; + } + + _.each(vm[scope], function (item) { + _.each(item[1].pg, function (index) { + index.checked = val; + }); + _.each(item[1].elasticsearch, function (index) { + index.checked = val; + }); + }); + } + + + vm.transformPartitionList = function (inputList) { + var outputList = []; + + _.each(inputList, function (item) { + var time = [item[0], { + elasticsearch: [], + pg: [] + }] + _.each(item[1].pg, function (index) { + time[1].pg.push({name: index, checked: false}) + }); + _.each(item[1].elasticsearch, function (index) { + time[1].elasticsearch.push({ + name: index, + checked: false + }) + }); + outputList.push(time); + }); + return outputList; + }; + + sectionViewResource.get({section:'admin_section', view: 'partitions'}, + vm.loadPartitions); + + vm.partitionsDelete = function (partitionType) { + var es_indices = []; + var pg_indices = []; + _.each(vm[partitionType], function (item) { + _.each(item[1].pg, function (index) { + if (index.checked) { + pg_indices.push(index.name) + } + }); + _.each(item[1].elasticsearch, function (index) { + if (index.checked) { + es_indices.push(index.name) + } + }); + }); + + + vm.loading = {partitions: true}; + sectionViewResource.save({section:'admin_section', + view: 'partitions_remove'}, { + es_indices: es_indices, + pg_indices: pg_indices, + confirm: 'CONFIRM' + }, vm.loadPartitions); + + } + +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers').controller('AdminSystemController', AdminSystemController); + +AdminSystemController.$inject = ['sectionViewResource']; + +function AdminSystemController(sectionViewResource) { + var vm = this; + vm.tables = []; + vm.loading = {system: true}; + sectionViewResource.get({ + section: 'admin_section', + view: 'system' + }, null, function (data) { + vm.DBtables = data.db_tables; + vm.ESIndices = data.es_indices; + vm.queueStats = data.queue_stats; + vm.systemLoad = data.system_load; + vm.packages = data.packages; + vm.processInfo = data.process_info; + vm.disks = data.disks; + vm.memory = data.memory; + vm.selfInfo = data.self_info; + + vm.loading.system = false; + }); +}; + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers').controller('AdminUsersCreateController', AdminUsersCreateController); + +AdminUsersCreateController.$inject = ['$state', 'usersResource', 'usersPropertyResource', 'sectionViewResource', 'AeConfig']; + +function AdminUsersCreateController($state, usersResource, usersPropertyResource, sectionViewResource, AeConfig) { + + var vm = this; + vm.loading = {user: false}; + + + if (typeof $state.params.userId !== 'undefined') { + vm.loading.user = true; + var userId = $state.params.userId; + vm.user = usersResource.get({userId: userId}, function (data) { + vm.loading.user = false; + // cast to true for angular checkbox + if (vm.user.status === 1) { + vm.user.status = true; + } + }); + + vm.resource_permissions = usersPropertyResource.query( + {userId: userId, key: 'resource_permissions'}, function (data) { + vm.loading.resource_permissions = false; + var tmpObj = { + 'user': { + 'application': {}, + 'dashboard': {} + }, + 'group': { + 'application': {}, + 'dashboard': {} + } + }; + _.each(data, function (item) { + + var section = tmpObj[item.type][item.resource_type]; + if (typeof section[item.resource_id] == 'undefined'){ + section[item.resource_id] = { + self:item, + permissions: [] + } + } + section[item.resource_id].permissions.push(item.perm_name); + + }); + vm.resourcePermissions = tmpObj; + }); + + } + else { + var userId = null; + vm.user = { + status: true + } + } + + var formResponse = function (response) { + if (response.status == 422) { + setServerValidation(vm.profileForm, response.data); + } + vm.loading.user = false; + } + + vm.createUser = function () { + vm.loading.user = true; + + if (userId) { + usersResource.update({userId: vm.user.id}, vm.user, function (data) { + setServerValidation(vm.profileForm); + vm.loading.user = false; + }, formResponse); + } + else { + usersResource.save(vm.user, function (data) { + $state.go('admin.user.update', {userId: data.id}); + }, formResponse); + } + } + + vm.generatePassword = function () { + var length = 8; + var charset = "abcdefghijklnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + vm.gen_pass = ""; + for (var i = 0, n = charset.length; i < length; ++i) { + vm.gen_pass += charset.charAt(Math.floor(Math.random() * n)); + } + vm.user.user_password = '' + vm.gen_pass; + + } + + vm.reloginUser = function () { + sectionViewResource.get({ + section: 'admin_section', view: 'relogin_user', + user_id: vm.user.id + }, function () { + window.location = AeConfig.urls.baseUrl; + }); + + } +}; + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers').controller('AdminUsersController', AdminUsersController); + +AdminUsersController.$inject = ['usersResource']; + +function AdminUsersController(usersResource) { + + var vm = this; + vm.loading = {users: true}; + + vm.users = usersResource.query({}, function (data) { + vm.loading = {users: false}; + vm.activeUsers = _.reduce(vm.users, function(memo, val){ + if (val.status == 1){ + return memo + 1; + } + return memo; + }, 0); + + }); + + + vm.removeUser = function (user) { + usersResource.remove({userId: user.id}, function (data, responseHeaders) { + + if (data) { + var index = vm.users.indexOf(user); + if (index !== -1) { + vm.users.splice(index, 1); + vm.activeUsers -= 1; + } + } + }); + } +}; + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('ApplicationsUpdateController', ApplicationsUpdateController) + +ApplicationsUpdateController.$inject = ['$state', 'applicationsNoIdResource', 'applicationsResource', 'applicationsPropertyResource', 'AeUser']; + +function ApplicationsUpdateController($state, applicationsNoIdResource, applicationsResource, applicationsPropertyResource, AeUser) { + 'use strict'; + + var vm = this; + vm.loading = {application: false}; + + vm.groupingOptions = [ + ['url_type', 'Error Type + location'], + ['url_traceback', 'Traceback + location'], + ['traceback_server', 'Traceback + Server'], + ]; + + var resourceId = $state.params.resourceId; + + + var options = {}; + + vm.momentJs = moment; + + vm.formTransferModel = {password:''}; + + // set initial data + + if (resourceId === 'new') { + vm.resource = { + resource_id: null, + slow_report_threshold: 10, + error_report_threshold: 10, + allow_permanent_storage: true, + default_grouping: vm.groupingOptions[1][0] + }; + } + else { + vm.loading.application = true; + vm.resource = applicationsResource.get({ + 'resourceId': resourceId + }, function (data) { + vm.loading.application = false; + }); + } + + + vm.updateBasicForm = function () { + vm.loading.application = true; + if (vm.resource.resource_id === null) { + applicationsNoIdResource.save(null, vm.resource, function (data) { + + AeUser.addApplication(data); + + $state.go('applications.update', {resourceId: data.resource_id}); + setServerValidation(vm.BasicForm); + }, function (response) { + if (response.status == 422) { + setServerValidation(vm.BasicForm, response.data); + } + vm.loading.application = false; + + }); + } + else { + applicationsResource.update({resourceId: vm.resource.resource_id}, + vm.resource, function (data) { + vm.resource = data; + vm.loading.application = false; + setServerValidation(vm.BasicForm); + }, function (response) { + if (response.status == 422) { + setServerValidation(vm.BasicForm, response.data); + } + vm.loading.application = false; + }); + } + }; + + vm.addRule = function () { + + applicationsPropertyResource.save({ + resourceId: vm.resource.resource_id, + key: 'postprocessing_rules' + }, null, + function (data) { + vm.resource.postprocessing_rules.push(data); + } + ); + }; + + vm.regenerateAPIKeys = function(){ + vm.loading.application = true; + applicationsPropertyResource.save({ + resourceId: vm.resource.resource_id, + key: 'api_key' + }, {password: vm.regenerateAPIKeysPassword}, + function (data) { + vm.resource = data; + vm.loading.application = false; + vm.regenerateAPIKeysPassword = ''; + setServerValidation(vm.regenerateAPIKeysForm); + }, + function (response) { + if (response.status == 422) { + setServerValidation(vm.regenerateAPIKeysForm, response.data); + + } + vm.loading.application = false; + } + ) + }; + + vm.deleteApplication = function(){ + vm.loading.application = true; + applicationsPropertyResource.update({ + resourceId: vm.resource.resource_id, + key: 'delete_resource' + }, vm.formDeleteModel, + function (data) { + + AeUser.removeApplicationById(vm.resource.resource_id); + + $state.go('applications.list'); + }, + function (response) { + if (response.status == 422) { + setServerValidation(vm.formDelete, response.data); + + } + vm.loading.application = false; + } + ); + }; + + vm.transferApplication = function(){ + vm.loading.application = true; + applicationsPropertyResource.update({ + resourceId: vm.resource.resource_id, + key: 'owner' + }, vm.formTransferModel, + function (data) { + $state.go('applications.list'); + }, + function (response) { + if (response.status == 422) { + setServerValidation(vm.formTransfer, response.data); + + } + vm.loading.application = false; + } + ) + } + +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('IntegrationController', IntegrationController) + +IntegrationController.$inject = ['$state', 'integrationResource']; + +function IntegrationController($state, integrationResource) { + + var vm = this; + vm.loading = {integration: true}; + vm.config = integrationResource.get( + { + integration: $state.params.integration, + action: 'setup', + resourceId: $state.params.resourceId + }, function (data) { + vm.loading.integration = false; + }); + + vm.configureIntegration = function () { + console.info('configureIntegration'); + vm.loading.integration = true; + integrationResource.save( + { + integration: $state.params.integration, + action: 'setup', + resourceId: $state.params.resourceId + }, vm.config, function (data) { + vm.loading.integration = false; + setServerValidation(vm.integrationForm); + }, function (response) { + if (response.status == 422) { + setServerValidation(vm.integrationForm, response.data); + } + vm.loading.integration = false; + }); + }; + + vm.removeIntegration = function () { + console.info('removeIntegration'); + integrationResource.remove({ + integration: $state.params.integration, + resourceId: $state.params.resourceId, + action: 'delete' + }, + function () { + $state.go('applications.integrations', + {resourceId: $state.params.resourceId}); + } + ); + } + + vm.testIntegration = function(to_test){ + console.info('testIntegration', to_test); + vm.loading.integration = true; + integrationResource.save( + { + integration: $state.params.integration, + action: 'test_'+ to_test, + resourceId: $state.params.resourceId + }, vm.config, function (data) { + vm.loading.integration = false; + }, function (response) { + vm.loading.integration = false; + }); + } + + +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('IntegrationsListController', IntegrationsListController) + +IntegrationsListController.$inject = ['$state', 'applicationsResource']; + +function IntegrationsListController($state, applicationsResource) { + + var vm = this; + vm.loading = {application: true}; + vm.resource = applicationsResource.get({resourceId: $state.params.resourceId}, function (data) { + vm.loading.application = false; + $state.current.data.resource = vm.resource; + }); +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('ApplicationsListController', ApplicationsListController) + +ApplicationsListController.$inject = ['applicationsResource']; + +function ApplicationsListController(applicationsResource) { + + var vm = this; + vm.loading = {applications: true}; + vm.applications = applicationsResource.query(null, function(){ + vm.loading.applications = false; + }); +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('ApplicationsPurgeLogsController', ApplicationsPurgeLogsController) + +ApplicationsPurgeLogsController.$inject = ['applicationsResource', 'sectionViewResource', 'logsNoIdResource']; + +function ApplicationsPurgeLogsController(applicationsResource, sectionViewResource, logsNoIdResource) { + + var vm = this; + vm.loading = {applications: true}; + + vm.namespace = null; + vm.selectedResource = null; + vm.commonNamespaces = []; + + vm.applications = applicationsResource.query({'type':'update_reports'}, function () { + vm.loading.applications = false; + vm.selectedResource = vm.applications[0].resource_id; + vm.getCommonKeys(); + }); + + /** + * Fetches most commonly used tags in logs + */ + vm.getCommonKeys = function () { + sectionViewResource.get({ + section: 'logs_section', + view: 'common_tags', + resource: vm.selectedResource + }, function (data) { + vm.commonNamespaces = data['namespaces'] + }); + }; + + vm.purgeLogs = function () { + vm.loading.applications = true; + logsNoIdResource.delete({resource:vm.selectedResource, + namespace: vm.namespace}, function(){ + vm.loading.applications = false; + }); + } +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers').controller('EventsController', EventsController); + +EventsController.$inject = ['eventsNoIdResource', 'eventsResource']; + +function EventsController(eventsNoIdResource, eventsResource) { + console.info('EventsController'); + var vm = this; + + vm.loading = {events: true}; + + vm.events = eventsNoIdResource.query( + {key: 'events'}, + function (data) { + vm.loading.events = false; + }); + + + vm.closeEvent = function (event) { + + eventsResource.update({eventId: event.id}, {status: 0}, function (data) { + event.status = 0; + }); + } + +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('IndexDashboardController', IndexDashboardController); + +IndexDashboardController.$inject = ['$scope', '$location','$cookies', '$interval', 'stateHolder', 'userSelfPropertyResource', 'applicationsPropertyResource', 'AeConfig', 'AeUser']; + +function IndexDashboardController($scope, $location, $cookies, $interval, stateHolder, userSelfPropertyResource, applicationsPropertyResource, AeConfig, AeUser) { + var vm = this; + stateHolder.section = 'dashboard'; + vm.timeOptions = {}; + var allowed = ['1h', '4h', '12h', '24h', '1w', '2w', '1M']; + _.each(allowed, function (key) { + if (allowed.indexOf(key) !== -1) { + vm.timeOptions[key] = AeConfig.timeOptions[key]; + } + }); + vm.urls = AeConfig.urls; + vm.applications = AeUser.applications_map; + vm.show_dashboard = false; + vm.resource = null; + vm.graphType = {selected: null}; + vm.timeSpan = vm.timeOptions['1h']; + vm.trendingReports = []; + vm.exceptions = 0; + vm.satisfyingRequests = 0; + vm.toleratedRequests = 0; + vm.frustratingRequests = 0; + vm.uptimeStats = 0; + vm.apdexStats = []; + vm.seriesRequestsData = []; + vm.seriesMetricsData = []; + vm.seriesSlowData = []; + vm.slowCalls = []; + vm.slowURIS = []; + + vm.reportChartConfig = { + data: { + json: [], + xFormat: '%Y-%m-%dT%H:%M:%S' + }, + color: { + pattern: ['#6baed6', '#e6550d', '#74c476', '#fdd0a2', '#8c564b'] + }, + axis: { + x: { + type: 'timeseries', + tick: { + culling: { + max: 6 // the number of tick texts will be adjusted to less than this value + }, + format: '%Y-%m-%d %H:%M' + } + }, + y: { + tick: { + count: 5, + format: d3.format('.2s') + } + } + }, + subchart: { + show: true, + size: { + height: 20 + } + }, + size: { + height: 250 + }, + zoom: { + rescale: true + }, + grid: { + x: { + show: true + }, + y: { + show: true + } + }, + tooltip: { + format: { + title: function (d) { + return '' + d; + }, + value: function (v) { + return v + } + } + } + }; + vm.reportChartData = {}; + + vm.reportSlowChartConfig = { + data: { + json: [], + xFormat: '%Y-%m-%dT%H:%M:%S' + }, + color: { + pattern: ['#6baed6', '#e6550d', '#74c476', '#fdd0a2', '#8c564b'] + }, + axis: { + x: { + type: 'timeseries', + tick: { + culling: { + max: 6 // the number of tick texts will be adjusted to less than this value + }, + format: '%Y-%m-%d %H:%M' + } + }, + y: { + tick: { + count: 5, + format: d3.format('.2s') + } + } + }, + subchart: { + show: true, + size: { + height: 20 + } + }, + size: { + height: 250 + }, + zoom: { + rescale: true + }, + grid: { + x: { + show: true + }, + y: { + show: true + } + }, + tooltip: { + format: { + title: function (d) { + return '' + d; + }, + value: function (v) { + return v + } + } + } + }; + vm.reportSlowChartData = {}; + + vm.metricsChartConfig = { + data: { + json: [], + xFormat: '%Y-%m-%dT%H:%M:%S', + keys: { + x: 'x', + value: ["main", "sql", "nosql", "tmpl", "remote", "custom"] + }, + names: { + main: 'View/Application logic', + sql: 'Relational database queries', + nosql: 'NoSql datastore calls', + tmpl: 'Template rendering', + custom: 'Custom timed calls', + remote: 'Requests to remote resources' + }, + type: 'area', + groups: [["main", "sql", "nosql", "remote", "custom", "tmpl"]], + order: null + }, + color: { + pattern: ['#6baed6', '#c7e9c0', '#fd8d3c', '#d6616b', '#ffcc00', '#c6dbef'] + }, + axis: { + x: { + type: 'timeseries', + tick: { + culling: { + max: 6 // the number of tick texts will be adjusted to less than this value + }, + format: '%Y-%m-%d %H:%M' + } + }, + y: { + tick: { + count: 5, + format: d3.format('.2f') + } + } + }, + point: { + show: false + }, + subchart: { + show: true, + size: { + height: 20 + } + }, + size: { + height: 350 + }, + zoom: { + rescale: true + }, + grid: { + x: { + show: true + }, + y: { + show: true + } + }, + tooltip: { + format: { + title: function (d) { + return '' + d; + }, + value: function (v) { + return v + } + } + } + }; + vm.metricsChartData = {}; + + vm.responseChartConfig = { + data: { + json: [], + xFormat: '%Y-%m-%dT%H:%M:%S' + }, + color: { + pattern: ['#d6616b', '#6baed6', '#fd8d3c'] + }, + axis: { + x: { + type: 'timeseries', + tick: { + culling: { + max: 6 // the number of tick texts will be adjusted to less than this value + }, + format: '%Y-%m-%d %H:%M' + } + }, + y: { + tick: { + count: 5, + format: d3.format('.2f') + } + } + }, + point: { + show: false + }, + subchart: { + show: true, + size: { + height: 20 + } + }, + size: { + height: 350 + }, + zoom: { + rescale: true + }, + grid: { + x: { + show: true + }, + y: { + show: true + } + }, + tooltip: { + format: { + title: function (d) { + return '' + d; + }, + value: function (v) { + return v + } + } + } + }; + vm.responseChartData = {}; + + vm.requestsChartConfig = { + data: { + json: [], + xFormat: '%Y-%m-%dT%H:%M:%S' + }, + color: { + pattern: ['#d6616b', '#6baed6', '#fd8d3c'] + }, + axis: { + x: { + type: 'timeseries', + tick: { + culling: { + max: 6 // the number of tick texts will be adjusted to less than this value + }, + format: '%Y-%m-%d %H:%M' + } + }, + y: { + tick: { + count: 5, + format: d3.format('.2f') + } + } + }, + point: { + show: false + }, + subchart: { + show: true, + size: { + height: 20 + } + }, + size: { + height: 350 + }, + zoom: { + rescale: true + }, + grid: { + x: { + show: true + }, + y: { + show: true + } + }, + tooltip: { + format: { + title: function (d) { + return '' + d; + }, + value: function (v) { + return v + } + } + } + }; + vm.requestsChartData = {}; + + vm.loading = { + 'apdex': true, + 'reports': true, + 'graphs': true, + 'slowCalls': true, + 'slowURIS': true, + 'requestsBreakdown': true, + 'series': true + }; + vm.stream = {paused: false, filtered: false, messages: [], reports: []}; + vm.websocket = null; + userSelfPropertyResource.get({key: 'websocket'}, function (data) { + + + vm.websocket = new ReconnectingWebSocket(AeConfig.ws_url + '/ws?conn_id=' + data.conn_id); + vm.websocket.onopen = function (event) { + + }; + vm.websocket.onmessage = function (event) { + var data = JSON.parse(event.data); + + $scope.$apply(function (scope) { + _.each(data, function (message) { + if (message.type === 'message'){ + ws_report = message.message.report; + if (ws_report.http_status != 500) { + return + } + if (vm.stream.paused == true) { + return + } + if (vm.stream.filtered && ws_report.resource_id != vm.resource) { + return + } + var should_insert = true; + _.each(vm.stream.reports, function (report) { + if (report.report_id == ws_report.report_id) { + report.occurences = ws_report.occurences; + should_insert = false; + } + }); + if (should_insert) { + if (vm.stream.reports.length > 7) { + vm.stream.reports.pop(); + } + vm.stream.reports.unshift(ws_report); + } + } + }); + }); + }; + vm.websocket.onclose = function (event) { + + }; + + vm.websocket.onerror = function (event) { + + }; + + }); + + vm.determineStartState = function () { + if (AeUser.applications.length) { + vm.resource = Number($location.search().resource); + + if (!vm.resource){ + var cookieResource = $cookies.getObject('resource'); + + + if (cookieResource) { + vm.resource = cookieResource; + } + else{ + vm.resource = AeUser.applications[0].resource_id; + } + } + } + + var timespan = $location.search().timespan; + + if(_.has(vm.timeOptions, timespan)){ + vm.timeSpan = vm.timeOptions[timespan]; + } + else{ + vm.timeSpan = vm.timeOptions['1h']; + } + + var graphType = $location.search().graphtype; + if(!graphType){ + vm.graphType = {selected: 'metrics_graphs'}; + } + else{ + vm.graphType = {selected: graphType}; + } + vm.updateSearchParams(); + }; + + vm.updateSearchParams = function () { + $location.search('resource', vm.resource); + $location.search('timespan', vm.timeSpan.key); + $location.search('graphtype', vm.graphType.selected); + stateHolder.resource = vm.resource; + if (vm.resource){ + $cookies.putObject('resource', vm.resource, + {expires:new Date(3000, 1, 1)}); + } + }; + + vm.refreshData = function () { + vm.fetchApdexStats(); + vm.fetchTrendingReports(); + vm.fetchMetrics(); + vm.fetchRequestsBreakdown(); + vm.fetchSlowCalls(); + } + + vm.changedTimeSpan = function(){ + vm.startDateFilter = timeSpanToStartDate(vm.timeSpan.key); + vm.refreshData(); + } + + var intervalId = $interval(function () { + if (_.contains(['30m', "1h"], vm.timeSpan.key)) { + vm.refreshData(); + } + }, 60000); + + $scope.$on('$destroy',function(){ + $interval.cancel(intervalId); + vm.websocket.close(); + }); + + + vm.fetchApdexStats = function () { + vm.loading.apdex = true; + vm.apdexStats = applicationsPropertyResource.query({ + 'key': 'apdex_stats', + 'resourceId': vm.resource, + "start_date": timeSpanToStartDate(vm.timeSpan.key) + }, + function (data) { + vm.loading.apdex = false; + + vm.exceptions = _.reduce(data, function (memo, row) { + return memo + row.errors; + }, 0); + vm.satisfyingRequests = _.reduce(data, function (memo, row) { + return memo + row.satisfying_requests; + }, 0); + vm.toleratedRequests = _.reduce(data, function (memo, row) { + return memo + row.tolerated_requests; + }, 0); + vm.frustratingRequests = _.reduce(data, function (memo, row) { + return memo + row.frustrating_requests; + }, 0); + if (data.length) { + vm.uptimeStats = data[0].uptime; + } + + }, + function () { + vm.loading.apdex = false; + } + ); + } + + vm.fetchMetrics = function () { + vm.loading.series = true; + applicationsPropertyResource.query({ + 'resourceId': vm.resource, + 'key': vm.graphType.selected, + "start_date": timeSpanToStartDate(vm.timeSpan.key) + }, function (data) { + if (vm.graphType.selected == 'metrics_graphs') { + vm.metricsChartData = { + json: data, + xFormat: '%Y-%m-%dT%H:%M:%S', + keys: { + x: 'x', + value: ["main", "sql", "nosql", "tmpl", "remote", "custom"] + }, + names: { + main: 'View/Application logic', + sql: 'Relational database queries', + nosql: 'NoSql datastore calls', + tmpl: 'Template rendering', + custom: 'Custom timed calls', + remote: 'Requests to remote resources' + }, + type: 'area', + groups: [["main", "sql", "nosql", "remote", "custom", "tmpl"]], + order: null + }; + } + else if (vm.graphType.selected == 'report_graphs') { + vm.reportChartData = { + json: data, + xFormat: '%Y-%m-%dT%H:%M:%S', + keys: { + x: 'x', + value: ["not_found", "report"] + }, + names: { + report: 'Errors', + not_found: '404\'s requests' + }, + type: 'bar' + }; + } + else if (vm.graphType.selected == 'slow_report_graphs') { + vm.reportSlowChartData = { + json: data, + xFormat: '%Y-%m-%dT%H:%M:%S', + keys: { + x: 'x', + value: ["slow_report"] + }, + names: { + slow_report: 'Slow reports' + }, + type: 'bar' + }; + } + else if (vm.graphType.selected == 'response_graphs') { + vm.responseChartData = { + json: data, + xFormat: '%Y-%m-%dT%H:%M:%S', + keys: { + x: 'x', + value: ["today", "days_ago_2", "days_ago_7"] + }, + names: { + today: 'Today', + "days_ago_2": '2 days ago', + "days_ago_7": '7 days ago' + } + }; + } + else if (vm.graphType.selected == 'requests_graphs') { + vm.requestsChartData = { + json: data, + xFormat: '%Y-%m-%dT%H:%M:%S', + keys: { + x: 'x', + value: ["requests"] + }, + names: { + requests: 'Requests/s' + } + }; + } + vm.loading.series = false; + }, function(){ + vm.loading.series = false; + }); + } + + vm.fetchSlowCalls = function () { + vm.loading.slowCalls = true; + applicationsPropertyResource.query({ + 'resourceId': vm.resource, + "start_date": timeSpanToStartDate(vm.timeSpan.key), + 'key': 'slow_calls' + }, function (data) { + vm.slowCalls = data; + vm.loading.slowCalls = false; + }, function () { + vm.loading.slowCalls = false; + }); + } + + vm.fetchRequestsBreakdown = function () { + vm.loading.requestsBreakdown = true; + applicationsPropertyResource.query({ + 'resourceId': vm.resource, + "start_date": timeSpanToStartDate(vm.timeSpan.key), + 'key': 'requests_breakdown' + }, function (data) { + vm.requestsBreakdown = data; + vm.loading.requestsBreakdown = false; + }, function () { + vm.loading.requestsBreakdown = false; + }); + } + + vm.fetchTrendingReports = function () { + + if (vm.graphType.selected == 'slow_report_graphs') { + var report_type = 'slow'; + } + else { + var report_type = 'error'; + } + + vm.loading.reports = true; + vm.trendingReports = applicationsPropertyResource.query({ + 'key': 'trending_reports', + 'resourceId': vm.resource, + "start_date": timeSpanToStartDate(vm.timeSpan.key), + "report_type": report_type + }, + function () { + vm.loading.reports = false; + }, + function () { + vm.loading.reports = false; + } + ) + ; + } + + if (AeUser.applications.length){ + vm.show_dashboard = true; + vm.determineStartState(); + vm.refreshData(); + } + + $scope.$on('$locationChangeSuccess', function () { + + if (vm.loading.series === false) { + vm.determineStartState(); + vm.refreshData(); + } + }); + + +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('HeaderCtrl', HeaderCtrl); + +HeaderCtrl.$inject = ['$state', 'stateHolder', 'AeUser']; + +function HeaderCtrl($state, stateHolder, AeUser) { + var vm = this; + vm.stateHolder = stateHolder; + vm.assignedReports = AeUser.assigned_reports; + vm.latestEvents = AeUser.latest_events; + vm.activeEvents = 0; + _.each(vm.latestEvents, function (event) { + if (event.status === 1 && event.end_date === null) { + vm.activeEvents += 1; + } + }); + + vm.clickedEvent = function(event){ + + // (from Event model) + // exception reports + if (_.contains([1,2], event.event_type)){ + $state.go('report.list', {resource:event.resource_id, start_date:event.start_date}); + } + // slowness reports + else if (_.contains([3,4], event.event_type)){ + $state.go('report.list_slow', {resource:event.resource_id, start_date:event.start_date}); + } + // uptime reports + else if (_.contains([7,8], event.event_type)){ + $state.go('uptime', {resource:event.resource_id, start_date:event.start_date}); + } + else{ + + } + } +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers').controller('IndexCtrl', IndexCtrl); + +IndexCtrl.$inject = [IndexCtrl]; + +function IndexCtrl() { + var vm = this; + vm.selected_section = 'errors'; +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('BitbucketIntegrationCtrl', BitbucketIntegrationCtrl) + +BitbucketIntegrationCtrl.$inject = ['$uibModalInstance', '$state', 'report', 'integrationName', 'integrationResource']; + +function BitbucketIntegrationCtrl($uibModalInstance, $state, report, integrationName, integrationResource) { + var vm = this; + vm.loading = true; + vm.assignees = []; + vm.report = report; + vm.integrationName = integrationName; + vm.statuses = []; + vm.priorities = []; + vm.error_messages = []; + vm.form = { + content: '\n' + + 'Issue created for report: ' + + $state.href('report.view_detail', {groupId:report.group_id, reportId:report.id}, {absolute:true}) + }; + + vm.fetchInfo = function () { + integrationResource.get({ + resourceId: vm.report.resource_id, + action: 'info', + integration: vm.integrationName + }, null, + function (data) { + vm.loading = false; + if (data.error_messages) { + vm.error_messages = data.error_messages; + } + vm.assignees = data.assignees; + vm.priorities = data.priorities; + vm.form.responsible = vm.assignees[0]; + vm.form.priority = vm.priorities[0]; + }, function (error_data) { + if (error_data.data.error_messages) { + vm.error_messages = error_data.data.error_messages; + } + else { + vm.error_messages = ['There was a problem processing your request']; + } + }); + }; + vm.ok = function () { + vm.loading = true; + vm.form.group_id = vm.report.group_id; + integrationResource.save({ + resourceId: vm.report.resource_id, + action: 'create-issue', + integration: vm.integrationName + }, vm.form, + function (data) { + vm.loading = false; + if (data.error_messages) { + vm.error_messages = data.error_messages; + } + if (data !== false) { + $uibModalInstance.dismiss('success'); + } + }, function (error_data) { + if (error_data.data.error_messages) { + vm.error_messages = error_data.data.error_messages; + } + else { + vm.error_messages = ['There was a problem processing your request']; + } + }); + }; + vm.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; + vm.fetchInfo(); +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('GithubIntegrationCtrl', GithubIntegrationCtrl); + +GithubIntegrationCtrl.$inject = ['$uibModalInstance', '$state', 'report', 'integrationName', 'integrationResource']; + +function GithubIntegrationCtrl($uibModalInstance, $state, report, integrationName, integrationResource) { + var vm = this; + vm.loading = true; + vm.assignees = []; + vm.report = report; + vm.integrationName = integrationName; + vm.statuses = []; + vm.assignees = []; + vm.error_messages = []; + vm.form = { + content: '\n' + + 'Issue created for report: ' + + $state.href('report.view_detail', {groupId:report.group_id, reportId:report.id}, {absolute:true}) + }; + + vm.fetchInfo = function () { + integrationResource.get({ + resourceId: vm.report.resource_id, + action: 'info', + integration: vm.integrationName + }, null, + function (data) { + vm.loading = false; + if (data.error_messages) { + vm.error_messages = data.error_messages; + } + else { + vm.assignees = data.assignees; + vm.statuses = data.statuses; + vm.form.responsible = vm.assignees[0]; + vm.form.status = vm.statuses[0]; + } + }, function (error_data) { + if (error_data.data.error_messages) { + vm.error_messages = error_data.data.error_messages; + } + else { + vm.error_messages = ['There was a problem processing your request']; + } + }); + }; + vm.ok = function () { + vm.loading = true; + vm.form.group_id = vm.report.group_id; + integrationResource.save({ + resourceId: vm.report.resource_id, + action: 'create-issue', + integration: vm.integrationName + }, vm.form, + function (data) { + vm.loading = false; + if (data.error_messages) { + vm.error_messages = data.error_messages; + } + else { + $uibModalInstance.dismiss('success'); + } + }, function (error_data) { + if (error_data.data.error_messages) { + vm.error_messages = error_data.data.error_messages; + } + else { + vm.error_messages = ['There was a problem processing your request']; + } + }); + }; + vm.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; + vm.fetchInfo(); +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('JiraIntegrationCtrl', JiraIntegrationCtrl) + +JiraIntegrationCtrl.$inject = ['$uibModalInstance', '$state', 'report', 'integrationName', 'integrationResource']; + +function JiraIntegrationCtrl($uibModalInstance, $state, report, integrationName, integrationResource) { + var vm = this; + vm.loading = true; + vm.assignees = []; + vm.report = report; + vm.integrationName = integrationName; + vm.statuses = []; + vm.priorities = []; + vm.error_messages = []; + vm.form = { + content: '\n' + + 'Issue created for report: ' + + $state.href('report.view_detail', {groupId:report.group_id, reportId:report.id}, {absolute:true}) + }; + + vm.fetchInfo = function () { + integrationResource.get({ + resourceId: vm.report.resource_id, + action: 'info', + integration: vm.integrationName + }, null, + function (data) { + vm.loading = false; + if (data.error_messages) { + vm.error_messages = data.error_messages; + } + vm.assignees = data.assignees; + vm.priorities = data.priorities; + vm.form.responsible = vm.assignees[0]; + vm.form.priority = vm.priorities[0]; + }, function (error_data) { + + if (error_data.data.error_messages) { + vm.error_messages = error_data.data.error_messages; + } + else { + vm.error_messages = ['There was a problem processing your request']; + } + }); + }; + vm.ok = function () { + vm.loading = true; + vm.form.group_id = vm.report.group_id; + integrationResource.save({ + resourceId: vm.report.resource_id, + action: 'create-issue', + integration: vm.integrationName + }, vm.form, + function (data) { + vm.loading = false; + if (data.error_messages) { + vm.error_messages = data.error_messages; + } + if (data !== false) { + $uibModalInstance.dismiss('success'); + } + }, function (error_data) { + if (error_data.data.error_messages) { + vm.error_messages = error_data.data.error_messages; + } + else { + vm.error_messages = ['There was a problem processing your request']; + } + }); + }; + vm.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; + vm.fetchInfo(); +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers').controller('LogsController', LogsController); + +LogsController.$inject = ['$scope', '$location', 'stateHolder', 'typeAheadTagHelper', 'logsNoIdResource', 'sectionViewResource', 'AeUser']; + +function LogsController($scope, $location, stateHolder, typeAheadTagHelper, logsNoIdResource, sectionViewResource, AeUser) { + var vm = this; + vm.logEventsChartConfig = { + data: { + json: [], + xFormat: '%Y-%m-%dT%H:%M:%S' + }, + color: { + pattern: ['#6baed6', '#e6550d', '#74c476', '#fdd0a2', '#8c564b'] + }, + axis: { + x: { + type: 'timeseries', + tick: { + format: '%Y-%m-%d' + } + }, + y: { + tick: { + count: 5, + format: d3.format('.2s') + } + } + }, + subchart: { + show: true, + size: { + height: 20 + } + }, + size: { + height: 250 + }, + zoom: { + rescale: true + }, + grid: { + x: { + show: true + }, + y: { + show: true + } + }, + tooltip: { + format: { + title: function (d) { + return '' + d; + }, + value: function (v) { + return v + } + } + } + }; + vm.logEventsChartData = {}; + stateHolder.section = 'logs'; + vm.today = function () { + vm.pickerDate = new Date(); + }; + vm.today(); + + vm.applications = AeUser.applications_map; + vm.logsPage = []; + vm.itemCount = 0; + vm.itemsPerPage = 250; + vm.searchParams = parseSearchToTags($location.search()); + vm.$location = $location; + vm.isLoading = { + logs: true, + series: true + }; + vm.filterTypeAheadOptions = [ + { + type: 'message', + text: 'message:', + 'description': 'Full-text search in your logs', + tag: 'Message', + example: 'message:text-im-looking-for' + }, + { + type: 'namespace', + text: 'namespace:', + 'description': 'Query logs from specific namespace', + tag: 'Namespace', + example: "namespace:module.foo" + }, + { + type: 'resource', + text: 'resource:', + 'description': 'Restrict resultset to application', + tag: 'Application', + example: "resource:ID" + }, + { + type: 'request_id', + text: 'request_id:', + 'description': 'Show logs with specific request id', + example: "request_id:883143dc572e4c38aceae92af0ea5ae0", + tag: 'Request ID' + }, + { + type: 'level', + text: 'level:', + 'description': 'Show entries with specific log level', + example: 'level:warning', + tag: 'Level' + }, + { + type: 'server_name', + text: 'server_name:', + 'description': 'Show entries tagged with this key/value pair', + example: 'server_name:hostname', + tag: 'Tag' + }, + { + type: 'start_date', + text: 'start_date:', + 'description': 'Show results newer than this date (press TAB for dropdown)', + example: 'start_date:2014-08-15T13:00', + tag: 'Start Date' + }, + { + type: 'end_date', + text: 'end_date:', + 'description': 'Show results older than this date (press TAB for dropdown)', + example: 'start_date:2014-08-15T23:59', + tag: 'End Date' + }, + {type: 'level', value: 'debug', text: 'level:debug'}, + {type: 'level', value: 'info', text: 'level:info'}, + {type: 'level', value: 'warning', text: 'level:warning'}, + {type: 'level', value: 'critical', text: 'level:critical'} + ]; + vm.filterTypeAhead = null; + vm.showDatePicker = false; + vm.manualOpen = false; + vm.aheadFilter = typeAheadTagHelper.aheadFilter; + vm.removeSearchTag = function (tag) { + $location.search(tag.type, null); + }; + vm.addSearchTag = function (tag) { + $location.search(tag.type, tag.value); + }; + + vm.paginationChange = function(){ + $location.search('page', vm.searchParams.page); + }; + + + _.each(vm.applications, function (item) { + vm.filterTypeAheadOptions.push({ + type: 'resource', + text: 'resource:' + item.resource_id + ':' + item.resource_name, + example: 'resource:' + item.resource_id, + 'tag': item.resource_name, + 'description': 'Restrict resultset to this application' + }); + }); + + vm.typeAheadTag = function (event) { + var text = vm.filterTypeAhead; + if (_.isObject(vm.filterTypeAhead)) { + text = vm.filterTypeAhead.text; + } + ; + if (!vm.filterTypeAhead) { + return + } + var parsed = text.split(':'); + var tag = {'type': null, 'value': null}; + // app tags have : twice + if (parsed.length > 2 && parsed[0] == 'resource') { + tag.type = 'resource'; + tag.value = parsed[1]; + } + // normal tag:value + else if (parsed.length > 1) { + tag.type = parsed[0]; + tag.value = parsed.slice(1).join(':'); + } + else { + tag.type = 'message'; + tag.value = parsed.join(':'); + } + + // set datepicker hour based on type of field + if ('start_date:' == text) { + vm.showDatePicker = true; + vm.filterTypeAhead = 'start_date:' + moment(vm.pickerDate).utc().format(); + } + else if ('end_date:' == text) { + vm.showDatePicker = true; + vm.filterTypeAhead = 'end_date:' + moment(vm.pickerDate).utc().hour(23).minute(59).format(); + } + + if (event.keyCode != 13 || !tag.type || !tag.value) { + return + } + vm.showDatePicker = false; + // aka we selected one of main options + $location.search(tag.type, tag.value); + + // clear typeahead + vm.filterTypeAhead = undefined; + }; + + + vm.pickerDateChanged = function(){ + if (vm.filterTypeAhead.indexOf('start_date:') == '0') { + vm.filterTypeAhead = 'start_date:' + moment(vm.pickerDate).utc().format(); + } + else if (vm.filterTypeAhead.indexOf('end_date:') == '0') { + vm.filterTypeAhead = 'end_date:' + moment(vm.pickerDate).utc().hour(23).minute(59).format(); + } + vm.showDatePicker = false; + }; + + vm.fetchLogs = function (searchParams) { + vm.isLoading.logs = true; + logsNoIdResource.query(searchParams, function (data, getResponseHeaders) { + vm.isLoading.logs = false; + var headers = getResponseHeaders(); + vm.logsPage = data; + vm.itemCount = headers['x-total-count']; + vm.itemsPerPage = headers['x-items-per-page']; + }, function () { + vm.isLoading.logs = false; + }); + }; + + vm.fetchSeriesData = function (searchParams) { + searchParams['section'] = 'logs_section'; + searchParams['view'] = 'fetch_series'; + vm.isLoading.series = true; + sectionViewResource.query(searchParams, function (data) { + + vm.logEventsChartData = { + json: data, + xFormat: '%Y-%m-%dT%H:%M:%S', + keys: { + x: 'x', + value: ["logs"] + }, + names: { + logs: 'Log events' + }, + type: 'bar' + }; + vm.isLoading.series = false; + }, function () { + vm.isLoading.series = false; + }); + }; + + vm.filterId = function (log) { + $location.search('request_id', log.request_id); + }; + + var params = parseTagsToSearch(vm.searchParams); + vm.fetchLogs(params); + vm.fetchSeriesData(params); + + $scope.$on('$locationChangeSuccess', function () { + + vm.searchParams = parseSearchToTags($location.search()); + var params = parseTagsToSearch(vm.searchParams); + + if (vm.isLoading.logs === false) { + + vm.fetchLogs(params); + vm.fetchSeriesData(params); + } + }); + +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('OverviewCtrl', OverviewCtrl); + +OverviewCtrl.$inject = []; + +function OverviewCtrl() { + +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('RegisterController', RegisterController); + +RegisterController.$inject = ['$scope', '$location']; + +function RegisterController() { + var vm = this; + vm.selected_form = 'sign-up'; + if (window.location.search.indexOf('sign_in') != -1 || window.location.search.indexOf('came_from') != -1) { + vm.selected_form = 'sign-in'; + } +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers').controller('AssignReportCtrl', AssignReportCtrl); +AssignReportCtrl.$inject = ['$uibModalInstance', 'reportGroupPropertyResource', 'report']; + +function AssignReportCtrl($uibModalInstance, reportGroupPropertyResource, report) { + var vm = this; + vm.loading = true; + vm.assignedUsers = []; + vm.unAssignedUsers = []; + vm.report = report; + vm.fetchAssignments = function () { + reportGroupPropertyResource.get({ + groupId: vm.report.group_id, + key: 'assigned_users' + }, null, + function (data) { + vm.assignedUsers = data.assigned; + vm.unAssignedUsers = data.unassigned; + vm.loading = false; + }); + } + + vm.reassignUser = function (user) { + var is_assigned = vm.assignedUsers.indexOf(user); + if (is_assigned != -1) { + vm.assignedUsers.splice(is_assigned, 1); + vm.unAssignedUsers.push(user); + return + } + var is_unassigned = vm.unAssignedUsers.indexOf(user); + if (is_unassigned != -1) { + vm.unAssignedUsers.splice(is_unassigned, 1); + vm.assignedUsers.push(user); + return + } + } + vm.updateAssignments = function () { + var post = {'unassigned': [], 'assigned': []}; + _.each(vm.assignedUsers, function (u) { + post['assigned'].push(u.user_name) + }); + _.each(vm.unAssignedUsers, function (u) { + post['unassigned'].push(u.user_name) + }); + vm.loading = true; + reportGroupPropertyResource.update({ + groupId: vm.report.group_id, + key: 'assigned_users' + }, post, + function (data) { + vm.loading = false; + $uibModalInstance.close(vm.report); + }); + }; + + + vm.ok = function () { + vm.updateAssignments(); + }; + + vm.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; + + vm.fetchAssignments(); + +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +'use strict'; + +/* Controllers */ + +angular.module('appenlight.controllers') + .controller('ReportsListSlowController', ReportsListSlowController); + +ReportsListSlowController.$inject = ['$scope', '$location', '$cookies', + 'stateHolder', 'typeAheadTagHelper', 'slowReportsResource', 'AeUser'] + +function ReportsListSlowController($scope, $location, $cookies, stateHolder, typeAheadTagHelper, slowReportsResource, AeUser) { + var vm = this; + vm.applications = AeUser.applications_map; + stateHolder.section = 'slow_reports'; + vm.today = function () { + vm.pickerDate = new Date(); + }; + vm.today(); + vm.reportsPage = []; + vm.itemCount = 0; + vm.itemsPerPage = 250; + typeAheadTagHelper.tags = []; + vm.searchParams = {tags: [], page: 1, type: 'slow_report'}; + vm.searchParams = parseSearchToTags($location.search()); + vm.is_loading = false; + vm.filterTypeAheadOptions = [ + { + type: 'view_name', + text: 'view_name:', + 'description': 'Query reports occured in specific views', + tag: 'View Name', + example: "view_name:module.foo" + }, + { + type: 'resource', + text: 'resource:', + 'description': 'Restrict resultset to application', + tag: 'Application', + example: "resource:ID" + }, + { + type: 'priority', + text: 'priority:', + 'description': 'Show reports with specific priority', + example: 'priority:8', + tag: 'Priority' + }, + { + type: 'min_occurences', + text: 'min_occurences:', + 'description': 'Show reports from groups with at least X occurences', + example: 'min_occurences:25', + tag: 'Min. occurences' + }, + { + type: 'min_duration', + text: 'min_duration:', + 'description': 'Show reports from groups with average duration >= Xs', + example: 'min_duration:4.5', + tag: 'Min. duration' + }, + { + type: 'url_path', + text: 'url_path:', + 'description': 'Show reports from specific URL paths', + example: 'url_path:/foo/bar/baz', + tag: 'Url Path' + }, + { + type: 'url_domain', + text: 'url_domain:', + 'description': 'Show reports from specific domain', + example: 'url_domain:domain.com', + tag: 'Domain' + }, + { + type: 'request_id', + text: 'request_id:', + 'description': 'Show reports with specific request id', + example: "request_id:883143dc572e4c38aceae92af0ea5ae0", + tag: 'Request ID' + }, + { + type: 'report_status', + text: 'report_status:', + 'description': 'Show reports from groups with specific status', + example: 'report_status:never_reviewed', + tag: 'Status' + }, + { + type: 'server_name', + text: 'server_name:', + 'description': 'Show reports tagged with this key/value pair', + example: 'server_name:hostname', + tag: 'Tag' + }, + { + type: 'start_date', + text: 'start_date:', + 'description': 'Show reports newer than this date (press TAB for dropdown)', + example: 'start_date:2014-08-15T13:00', + tag: 'Start Date' + }, + { + type: 'end_date', + text: 'end_date:', + 'description': 'Show reports older than this date (press TAB for dropdown)', + example: 'start_date:2014-08-15T23:59', + tag: 'End Date' + } + ]; + + vm.filterTypeAhead = undefined; + vm.showDatePicker = false; + vm.aheadFilter = typeAheadTagHelper.aheadFilter; + vm.removeSearchTag = function (tag) { + $location.search(tag.type, null); + }; + vm.addSearchTag = function (tag) { + $location.search(tag.type, tag.value); + }; + vm.manualOpen = false; + vm.notRelativeTime = false; + if ($cookies.notRelativeTime) { + vm.notRelativeTime = JSON.parse($cookies.notRelativeTime); + } + + + vm.changeRelativeTime = function () { + $cookies.notRelativeTime = JSON.stringify(vm.notRelativeTime); + }; + + _.each(_.range(1, 11), function (priority) { + vm.filterTypeAheadOptions.push({ + type: 'priority', + text: 'priority:' + priority.toString(), + description: 'Show entries with specific priority', + example: 'priority:' + priority, + tag: 'Priority' + }); + }); + _.each(['never_reviewed', 'reviewed', 'fixed', 'public'], function (status) { + vm.filterTypeAheadOptions.push({ + type: 'report_status', + text: 'report_status:' + status, + 'description': 'Show only reports with this status', + example: 'report_status:' + status, + tag: 'Status ' + status.toUpperCase() + }); + }); + _.each(AeUser.applications, function (item) { + vm.filterTypeAheadOptions.push({ + type: 'resource', + text: 'resource:' + item.resource_id + ':' + item.resource_name, + example: 'resource:' + item.resource_id, + 'tag': item.resource_name, + 'description': 'Restrict resultset to this application' + }); + }); + + vm.typeAheadTag = function (event) { + var text = vm.filterTypeAhead; + if (_.isObject(vm.filterTypeAhead)) { + text = vm.filterTypeAhead.text; + } + ; + if (!vm.filterTypeAhead) { + return + } + var parsed = text.split(':'); + var tag = {'type': null, 'value': null}; + // app tags have : twice + if (parsed.length > 2 && parsed[0] == 'resource') { + tag.type = 'resource'; + tag.value = parsed[1]; + } + // normal tag:value + else if (parsed.length > 1) { + tag.type = parsed[0]; + var tagValue = parsed.slice(1); + if (tagValue) { + tag.value = tagValue.join(':'); + } + } + + // set datepicker hour based on type of field + if ('start_date:' == text) { + vm.showDatePicker = true; + vm.filterTypeAhead = 'start_date:' + moment(vm.pickerDate).utc().format(); + } + else if ('end_date:' == text) { + vm.showDatePicker = true; + vm.filterTypeAhead = 'end_date:' + moment(vm.pickerDate).utc().hour(23).minute(59).format(); + } + + if (event.keyCode != 13 || !tag.type || !tag.value) { + return + } + vm.showDatePicker = false; + // aka we selected one of main options + $location.search(tag.type, tag.value); + // clear typeahead + vm.filterTypeAhead = undefined; + } + + vm.paginationChange = function(){ + $location.search('page', vm.searchParams.page); + } + + vm.pickerDateChanged = function(){ + if (vm.filterTypeAhead.indexOf('start_date:') == '0') { + vm.filterTypeAhead = 'start_date:' + moment(vm.pickerDate).utc().format(); + } + else if (vm.filterTypeAhead.indexOf('end_date:') == '0') { + vm.filterTypeAhead = 'end_date:' + moment(vm.pickerDate).utc().hour(23).minute(59).format(); + } + vm.showDatePicker = false; + } + + var reportPresentation = function (report) { + report.presentation = {}; + if (report.group.public) { + report.presentation.className = 'public'; + report.presentation.tooltip = 'Public'; + } + else if (report.group.fixed) { + report.presentation.className = 'fixed'; + report.presentation.tooltip = 'Fixed'; + } + else if (report.group.read) { + report.presentation.className = 'reviewed'; + report.presentation.tooltip = 'Reviewed'; + } + else { + report.presentation.className = 'new'; + report.presentation.tooltip = 'New'; + } + return report; + } + + vm.fetchReports = function (searchParams) { + vm.is_loading = true; + slowReportsResource.query(searchParams, function (data, getResponseHeaders) { + var headers = getResponseHeaders(); + + vm.is_loading = false; + vm.reportsPage = _.map(data, function (item) { + return reportPresentation(item); + }); + vm.itemCount = headers['x-total-count']; + vm.itemsPerPage = headers['x-items-per-page']; + }, function () { + vm.is_loading = false; + }); + } + + vm.filterId = function (log) { + vm.searchParams.tags.push({ + type: "request_id", + value: log.request_id + }); + } + //initial load + var params = parseTagsToSearch(vm.searchParams); + vm.fetchReports(params); + + $scope.$on('$locationChangeSuccess', function () { + + vm.searchParams = parseSearchToTags($location.search()); + var params = parseTagsToSearch(vm.searchParams); + + if (vm.is_loading === false) { + + vm.fetchReports(params); + } + }); + + +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('ReportsListController', ReportsListController); + +ReportsListController.$inject = ['$scope', '$location', '$cookies', + 'stateHolder', 'typeAheadTagHelper', 'reportsResource', 'AeUser']; + +function ReportsListController($scope, $location, $cookies, stateHolder, + typeAheadTagHelper, reportsResource, AeUser) { + var vm = this; + vm.applications = AeUser.applications_map; + stateHolder.section = 'reports'; + vm.today = function () { + vm.pickerDate = new Date(); + }; + vm.today(); + vm.reportsPage = []; + vm.itemCount = 0; + vm.itemsPerPage = 250; + typeAheadTagHelper.tags = []; + vm.searchParams = {tags: [], page: 1, type: 'report'}; + vm.searchParams = parseSearchToTags($location.search()); + vm.is_loading = false; + vm.filterTypeAheadOptions = [ + { + type: 'error', + text: 'error:', + 'description': 'Full-text search in your reports', + example: 'error:text-im-looking-for', + tag: 'Error' + }, + { + type: 'view_name', + text: 'view_name:', + 'description': 'Query reports occured in specific views', + example: "view_name:module.foo", + tag: 'View Name' + }, + { + type: 'resource', + text: 'resource:', + 'description': 'Restrict resultset to application', + example: "resource:ID", + tag: 'Application' + }, + { + type: 'priority', + text: 'priority:', + 'description': 'Show reports with specific priority', + example: 'priority:8', + tag: 'Priority' + }, + { + type: 'min_occurences', + text: 'min_occurences:', + 'description': 'Show reports from groups with at least X occurences', + example: 'min_occurences:25', + tag: 'Occurences' + }, + { + type: 'url_path', + text: 'url_path:', + 'description': 'Show reports from specific URL paths', + example: 'url_path:/foo/bar/baz', + tag: 'Url Path' + }, + { + type: 'url_domain', + text: 'url_domain:', + 'description': 'Show reports from specific domain', + example: 'url_domain:domain.com', + tag: 'Domain' + }, + { + type: 'report_status', + text: 'report_status:', + 'description': 'Show reports from groups with specific status', + example: 'report_status:never_reviewed', + tag: 'Status' + }, + { + type: 'request_id', + text: 'request_id:', + 'description': 'Show reports with specific request id', + example: "request_id:883143dc572e4c38aceae92af0ea5ae0", + tag: 'Request ID' + }, + { + type: 'server_name', + text: 'server_name:', + 'description': 'Show reports tagged with this key/value pair', + example: 'server_name:hostname', + tag: 'Tag' + }, + { + type: 'http_status', + text: 'http_status:', + 'description': 'Show reports with specific HTTP status code', + example: "http_status:", + tag: 'HTTP Status' + }, + { + type: 'http_status', + text: 'http_status:500', + 'description': 'Show reports with specific HTTP status code', + example: "http_status:500", + tag: 'HTTP Status' + }, + { + type: 'http_status', + text: 'http_status:404', + 'description': 'Include 404 reports in your search', + example: "http_status:404", + tag: 'HTTP Status' + }, + { + type: 'start_date', + text: 'start_date:', + 'description': 'Show reports newer than this date (press TAB for dropdown)', + example: 'start_date:2014-08-15T13:00', + tag: 'Start Date' + }, + { + type: 'end_date', + text: 'end_date:', + 'description': 'Show reports older than this date (press TAB for dropdown)', + example: 'start_date:2014-08-15T23:59', + tag: 'End Date' + } + ]; + + vm.filterTypeAhead = undefined; + vm.showDatePicker = false; + vm.manualOpen = false; + vm.aheadFilter = typeAheadTagHelper.aheadFilter; + vm.removeSearchTag = function (tag) { + $location.search(tag.type, null); + }; + vm.addSearchTag = function (tag) { + $location.search(tag.type, tag.value); + }; + vm.notRelativeTime = false; + if ($cookies.notRelativeTime) { + vm.notRelativeTime = JSON.parse($cookies.notRelativeTime); + } + + vm.changeRelativeTime = function () { + $cookies.notRelativeTime = JSON.stringify(vm.notRelativeTime); + } + + _.each(_.range(1, 11), function (priority) { + vm.filterTypeAheadOptions.push({ + type: 'priority', + text: 'priority:' + priority.toString(), + description: 'Show entries with specific priority', + example: 'priority:' + priority, + tag: 'Priority' + }); + }); + _.each(['never_reviewed', 'reviewed', 'fixed', 'public'], function (status) { + vm.filterTypeAheadOptions.push({ + type: 'report_status', + text: 'report_status:' + status, + 'description': 'Show only reports with this status', + example: 'report_status:' + status, + tag: 'Status ' + status.toUpperCase() + }); + }); + _.each(AeUser.applications, function (item) { + vm.filterTypeAheadOptions.push({ + type: 'resource', + text: 'resource:' + item.resource_id + ':' + item.resource_name, + example: 'resource:' + item.resource_id, + 'tag': item.resource_name, + 'description': 'Restrict resultset to this application' + }); + }); + + vm.paginationChange = function(){ + $location.search('page', vm.searchParams.page); + }; + + vm.typeAheadTag = function (event) { + var text = vm.filterTypeAhead; + if (_.isObject(vm.filterTypeAhead)) { + text = vm.filterTypeAhead.text; + } + if (!vm.filterTypeAhead) { + return + } + + var parsed = text.split(':'); + var tag = {'type': null, 'value': null}; + // app tags have : twice + if (parsed.length > 2 && parsed[0] == 'resource') { + tag.type = 'resource'; + tag.value = parsed[1]; + } + // normal tag:value + else if (parsed.length > 1) { + tag.type = parsed[0]; + var tagValue = parsed.slice(1); + if (tagValue) { + tag.value = tagValue.join(':'); + } + } + else { + tag.type = 'error'; + tag.value = parsed.join(':'); + } + + // set datepicker hour based on type of field + if ('start_date:' == text) { + vm.showDatePicker = true; + vm.filterTypeAhead = 'start_date:' + moment(vm.pickerDate).utc().format(); + } + else if ('end_date:' == text) { + vm.showDatePicker = true; + vm.filterTypeAhead = 'end_date:' + moment(vm.pickerDate).utc().hour(23).minute(59).format(); + } + + if (event.keyCode != 13 || !tag.type || !tag.value) { + return + } + vm.showDatePicker = false; + // aka we selected one of main options + $location.search(tag.type, tag.value); + // clear typeahead + vm.filterTypeAhead = undefined; + } + + vm.pickerDateChanged = function(){ + if (vm.filterTypeAhead.indexOf('start_date:') == '0') { + vm.filterTypeAhead = 'start_date:' + moment(vm.pickerDate).utc().format(); + } + else if (vm.filterTypeAhead.indexOf('end_date:') == '0') { + vm.filterTypeAhead = 'end_date:' + moment(vm.pickerDate).utc().hour(23).minute(59).format(); + } + vm.showDatePicker = false; + }; + + var reportPresentation = function (report) { + report.presentation = {}; + if (report.group.public) { + report.presentation.className = 'public'; + report.presentation.tooltip = 'Public'; + } + else if (report.group.fixed) { + report.presentation.className = 'fixed'; + report.presentation.tooltip = 'Fixed'; + } + else if (report.group.read) { + report.presentation.className = 'reviewed'; + report.presentation.tooltip = 'Reviewed'; + } + else { + report.presentation.className = 'new'; + report.presentation.tooltip = 'New'; + } + return report; + }; + + vm.fetchReports = function (searchParams) { + vm.is_loading = true; + reportsResource.query(searchParams, function (data, getResponseHeaders) { + var headers = getResponseHeaders(); + + vm.is_loading = false; + vm.reportsPage = _.map(data, function (item) { + return reportPresentation(item); + }); + vm.itemCount = headers['x-total-count']; + vm.itemsPerPage = headers['x-items-per-page']; + }, function () { + vm.is_loading = false; + }); + }; + + vm.filterId = function (log) { + vm.searchParams.tags.push({ + type: "request_id", + value: log.request_id + }); + }; + // initial load + var params = parseTagsToSearch(vm.searchParams); + vm.fetchReports(params); + + $scope.$on('$locationChangeSuccess', function () { + + vm.searchParams = parseSearchToTags($location.search()); + var params = parseTagsToSearch(vm.searchParams); + + if (vm.is_loading === false) { + + vm.fetchReports(params); + } + + }); + + +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers').controller('ReportsViewController', ReportsViewController); +ReportsViewController.$inject = ['$window', '$location', '$state', '$uibModal', + '$cookies', 'reportGroupPropertyResource', 'reportGroupResource', + 'logsNoIdResource', 'AeUser']; + +function ReportsViewController($window, $location, $state, $uibModal, $cookies, reportGroupPropertyResource, reportGroupResource, logsNoIdResource, AeUser) { + var vm = this; + vm.window = $window; + vm.reportHistoryConfig = { + data: { + json: [], + xFormat: '%Y-%m-%dT%H:%M:%S' + }, + color: { + pattern: ['#6baed6', '#e6550d', '#74c476', '#fdd0a2', '#8c564b'] + }, + axis: { + x: { + type: 'timeseries', + tick: { + format: '%Y-%m-%d' + } + }, + y: { + tick: { + count: 5, + format: d3.format('.2s') + } + } + }, + subchart: { + show: true, + size: { + height: 20 + } + }, + size: { + height: 250 + }, + zoom: { + rescale: true + }, + grid: { + x: { + show: true + }, + y: { + show: true + } + }, + tooltip: { + format: { + title: function (d) { + return '' + d; + }, + value: function (v) { + return v + } + } + } + }; + vm.mentionedPeople = []; + vm.reportHistoryData = {}; + vm.textTraceback = true; + vm.rawTraceback = ''; + vm.traceback = ''; + vm.reportType = 'report'; + vm.report = null; + vm.showLong = false; + vm.reportLogs = null; + vm.requestStats = null; + vm.comment = null; + vm.is_loading = { + report: true, + logs: true, + history: true + }; + + vm.searchMentionedPeople = function(term){ + //vm.mentionedPeople = []; + var term = term.toLowerCase(); + reportGroupPropertyResource.get({ + groupId: vm.report.group_id, + key: 'assigned_users' + }, null, + function (data) { + var users = []; + _.each(data.assigned, function(u){ + users.push({label: u.user_name}); + }); + _.each(data.unassigned, function(u){ + users.push({label: u.user_name}); + }); + + var result = _.filter(users, function(u){ + return u.label.toLowerCase().indexOf(term) !== -1; + }); + vm.mentionedPeople = result; + }); + }; + + vm.searchTag = function (tag, value) { + + if (vm.report.report_type === 3) { + $location.url($state.href('report.list_slow')); + } + else { + $location.url($state.href('report.list')); + } + $location.search(tag, value); + }; + + vm.tabs = { + slow_calls:false, + request_details:false, + logs:false, + comments:false, + affected_users:false + }; + if ($cookies.selectedReportTab) { + vm.tabs[$cookies.selectedReportTab] = true; + } + else{ + $cookies.selectedReportTab = 'request_details'; + vm.tabs.request_details = true; + } + + vm.fetchLogs = function () { + if (!vm.report.request_id){ + return + } + vm.is_loading.logs = true; + logsNoIdResource.query({request_id: vm.report.request_id}, + function (data) { + vm.is_loading.logs = false; + vm.reportLogs = data; + }, function () { + vm.is_loading.logs = false; + }); + }; + vm.addComment = function () { + reportGroupPropertyResource.save({ + groupId: vm.report.group_id, + key: 'comments' + }, {body: vm.comment}, + function (data) { + vm.report.comments.push(data); + }); + vm.comment = ''; + }; + + vm.fetchReport = function () { + vm.is_loading.report = true; + reportGroupResource.get($state.params, function (data) { + vm.is_loading.report = false; + if (data.request) { + try { + var to_sort = _.pairs(data.request); + data.request = _.object(_.sortBy(to_sort, function (i) { + return i[0] + })); + } + catch (err) { + } + } + vm.report = data; + if (vm.report.req_stats) { + vm.requestStats = []; + _.each(_.pairs(vm.report.req_stats['percentages']), function (p) { + vm.requestStats.push({ + name: p[0], + value: vm.report.req_stats[p[0]].toFixed(3), + percent: p[1], + calls: vm.report.req_stats[p[0] + '_calls'] + }) + }); + } + vm.traceback = data.traceback; + _.each(vm.traceback, function (frame) { + if (frame.line) { + vm.rawTraceback += 'File ' + frame.file + ' line ' + frame.line + ' in ' + frame.fn + ": \r\n"; + } + vm.rawTraceback += ' ' + frame.cline + "\r\n"; + }); + + if (AeUser.id){ + vm.fetchHistory(); + } + + vm.selectedTab($cookies.selectedReportTab); + + }, function (response) { + + if (response.status == 403) { + var uid = response.headers('x-appenlight-uid'); + if (!uid) { + window.location = '/register?came_from=' + encodeURIComponent(window.location); + } + } + vm.is_loading.report = false; + }); + }; + + vm.selectedTab = function(tab_name){ + $cookies.selectedReportTab = tab_name; + if (tab_name == 'logs' && vm.reportLogs === null) { + vm.fetchLogs(); + } + }; + + vm.markFixed = function () { + reportGroupResource.update({ + groupId: vm.report.group_id + }, {fixed: !vm.report.group.fixed}, + function (data) { + vm.report.group.fixed = data.fixed; + }); + }; + + vm.markPublic = function () { + reportGroupResource.update({ + groupId: vm.report.group_id + }, {public: !vm.report.group.public}, + function (data) { + vm.report.group.public = data.public; + }); + }; + + vm.delete = function () { + reportGroupResource.delete({'groupId': vm.report.group_id}, + function (data) { + $state.go('report.list'); + }) + }; + + vm.assignUsersModal = function (index) { + vm.opts = { + backdrop: 'static', + templateUrl: 'AssignReportCtrl.html', + controller: 'AssignReportCtrl as ctrl', + resolve: { + report: function () { + return vm.report; + } + } + }; + var modalInstance = $uibModal.open(vm.opts); + modalInstance.result.then(function (report) { + + }, function () { + console.info('Modal dismissed at: ' + new Date()); + }); + + }; + + vm.fetchHistory = function () { + reportGroupPropertyResource.query({ + groupId: vm.report.group_id, + key: 'history' + }, function (data) { + vm.reportHistoryData = { + json: data, + keys: { + x: 'x', + value: ["reports"] + }, + names: { + reports: 'Reports history' + }, + type: 'bar' + }; + vm.is_loading.history = false; + }); + }; + + vm.nextDetail = function () { + $state.go('report.view_detail', { + groupId: vm.report.group_id, + reportId: vm.report.group.next_report + }); + }; + vm.previousDetail = function () { + $state.go('report.view_detail', { + groupId: vm.report.group_id, + reportId: vm.report.group.previous_report + }); + }; + + vm.runIntegration = function (integration_name) { + + if (integration_name == 'bitbucket') { + var controller = 'BitbucketIntegrationCtrl as ctrl'; + var template_url = 'templates/integrations/bitbucket.html'; + } + else if (integration_name == 'github') { + var controller = 'GithubIntegrationCtrl as ctrl'; + var template_url = 'templates/integrations/github.html'; + } + else if (integration_name == 'jira') { + var controller = 'JiraIntegrationCtrl as ctrl'; + var template_url = 'templates/integrations/jira.html'; + } + else { + return false; + } + + vm.opts = { + backdrop: 'static', + templateUrl: template_url, + controller: controller, + resolve: { + integrationName: function () { + return integration_name + }, + report: function () { + return vm.report; + } + } + }; + var modalInstance = $uibModal.open(vm.opts); + modalInstance.result.then(function (report) { + + }, function () { + console.info('Modal dismissed at: ' + new Date()); + }); + + }; + + // load report + vm.fetchReport(); + + +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('AlertChannelsEmailController', AlertChannelsEmailController) + +AlertChannelsEmailController.$inject = ['$state','userSelfPropertyResource']; + +function AlertChannelsEmailController($state, userSelfPropertyResource) { + + var vm = this; + vm.loading = {email: false}; + vm.form = {}; + + vm.createChannel = function () { + vm.loading.email = true; + + userSelfPropertyResource.save({key: 'alert_channels'}, vm.form, function () { + //vm.loading.email = false; + //setServerValidation(vm.channelForm); + //vm.form = {}; + $state.go('user.alert_channels.list'); + }, function (response) { + if (response.status == 422) { + setServerValidation(vm.channelForm, response.data); + } + vm.loading.email = false; + }); + } +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers').controller('AlertChannelsController', AlertChannelsController); + +AlertChannelsController.$inject = ['userSelfPropertyResource', 'applicationsNoIdResource']; + +function AlertChannelsController(userSelfPropertyResource, applicationsNoIdResource) { + + var vm = this; + vm.loading = {channels: true, applications: true, actions:true}; + + vm.alertChannels = userSelfPropertyResource.query({key: 'alert_channels'}, + function (data) { + vm.loading.channels = false; + }); + + vm.alertActions = userSelfPropertyResource.query({key: 'alert_actions'}, + function (data) { + vm.loading.actions = false; + }); + + vm.applications = applicationsNoIdResource.query({permission: 'view'}, + function (data) { + vm.loading.applications = false; + }); + + var allOps = { + 'eq': 'Equal', + 'ne': 'Not equal', + 'ge': 'Greater or equal', + 'gt': 'Greater than', + 'le': 'Lesser or equal', + 'lt': 'Lesser than', + 'startswith': 'Starts with', + 'endswith': 'Ends with', + 'contains': 'Contains' + }; + + var fieldOps = {}; + fieldOps['http_status'] = ['eq', 'ne', 'ge', 'le']; + fieldOps['group:priority'] = ['eq', 'ne', 'ge', 'le']; + fieldOps['duration'] = ['ge', 'le']; + fieldOps['url_domain'] = ['eq', 'ne', 'startswith', 'endswith', + 'contains']; + fieldOps['url_path'] = ['eq', 'ne', 'startswith', 'endswith', + 'contains']; + fieldOps['error'] = ['eq', 'ne', 'startswith', 'endswith', + 'contains']; + fieldOps['tags:server_name'] = ['eq', 'ne', 'startswith', 'endswith', + 'contains']; + fieldOps['group:occurences'] = ['eq', 'ne', 'ge', 'le']; + + var possibleFields = { + '__AND__': 'All met (composite rule)', + '__OR__': 'One met (composite rule)', + 'http_status': 'HTTP Status', + 'duration': 'Request duration', + 'group:priority': 'Group -> Priority', + 'url_domain': 'Domain', + 'url_path': 'URL Path', + 'error': 'Error', + 'tags:server_name': 'Tag -> Server name', + 'group:occurences': 'Group -> Occurences' + }; + + vm.ruleDefinitions = { + fieldOps: fieldOps, + allOps: allOps, + possibleFields: possibleFields + }; + + vm.addAction = function (channel) { + + userSelfPropertyResource.save({key: 'alert_channels_rules'}, {}, function (data) { + vm.alertActions.push(data); + }, function (response) { + if (response.status == 422) { + + } + }); + }; + + vm.updateChannel = function (channel, subKey) { + var params = { + key: 'alert_channels', + channel_name: channel['channel_name'], + channel_value: channel['channel_value'] + }; + var toUpdate = {}; + if (['daily_digest', 'send_alerts'].indexOf(subKey) !== -1) { + toUpdate[subKey] = !channel[subKey]; + } + userSelfPropertyResource.update(params, toUpdate, function (data) { + _.extend(channel, data); + }); + }; + + vm.removeChannel = function (channel) { + + userSelfPropertyResource.delete({ + key: 'alert_channels', + channel_name: channel.channel_name, + channel_value: channel.channel_value + }, function () { + vm.alertChannels = _.filter(vm.alertChannels, function(item){ + return item != channel; + }); + }); + + } + +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers').controller('UserAuthTokensController', UserAuthTokensController); + +UserAuthTokensController.$inject = ['$filter', 'userSelfPropertyResource', 'AeConfig']; + +function UserAuthTokensController($filter, userSelfPropertyResource, AeConfig) { + + var vm = this; + vm.loading = {tokens: true}; + + vm.expireOptions = AeConfig.timeOptions; + + vm.tokens = userSelfPropertyResource.query({key: 'auth_tokens'}, + function (data) { + vm.loading.tokens = false; + }); + + vm.addToken = function () { + vm.loading.tokens = true; + userSelfPropertyResource.save({key: 'auth_tokens'}, + vm.form, + function (data) { + vm.loading.tokens = false; + setServerValidation(vm.TokenForm); + vm.form = {}; + vm.tokens.push(data); + }, function (response) { + vm.loading.tokens = false; + if (response.status == 422) { + setServerValidation(vm.TokenForm, response.data); + } + }) + } + + vm.removeToken = function (token) { + userSelfPropertyResource.delete({key: 'auth_tokens', + token:token.token}, + function () { + var index = vm.tokens.indexOf(token); + if (index !== -1) { + vm.tokens.splice(index, 1); + } + }) + } +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('UserIdentitiesController', UserIdentitiesController) + +UserIdentitiesController.$inject = ['userSelfPropertyResource']; + +function UserIdentitiesController(userSelfPropertyResource) { + + var vm = this; + vm.loading = {identities: true}; + + vm.identities = userSelfPropertyResource.query( + {key: 'external_identities'}, + function (data) { + vm.loading.identities = false; + + }); + + vm.removeProvider = function (provider) { + + userSelfPropertyResource.delete( + { + key: 'external_identities', + provider: provider.provider, + id: provider.id + }, + function (status) { + if (status){ + vm.identities = _.filter(vm.identities, function (item) { + return item != provider + }); + } + + }); + } +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('UserPasswordController', UserPasswordController) + +UserPasswordController.$inject = ['userSelfPropertyResource']; + +function UserPasswordController(userSelfPropertyResource) { + + var vm = this; + vm.loading = {password: false}; + vm.form = {}; + + vm.updatePassword = function () { + vm.loading.password = true; + + userSelfPropertyResource.update({key: 'password'}, vm.form, function () { + vm.loading.password = false; + vm.form = {}; + setServerValidation(vm.passwordForm); + }, function (response) { + if (response.status == 422) { + + setServerValidation(vm.passwordForm, response.data); + + } + vm.loading.password = false; + }); + } +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('UserProfileController', UserProfileController) + +UserProfileController.$inject = ['userSelfResource']; + +function UserProfileController(userSelfResource) { + + var vm = this; + vm.loading = {profile: true}; + + vm.user = userSelfResource.get(null, function (data) { + vm.loading.profile = false; + + }); + + vm.updateProfile = function () { + vm.loading.profile = true; + + + vm.user.$update(null, function () { + vm.loading.profile = false; + setServerValidation(vm.profileForm); + }, function (response) { + if (response.status == 422) { + setServerValidation(vm.profileForm, response.data); + } + vm.loading.profile = false; + }); + } +} + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.appVersion', []). + directive('appVersion', ['version', function (version) { + return function (scope, elm, attrs) { + elm.text(version); + }; + }]) +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +// This code is inspired by https://github.com/jettro/c3-angular-sample/tree/master/js +// License is MIT + + +angular.module('appenlight.directives.c3chart', []) + .controller('ChartCtrl', ['$scope', '$timeout', function ($scope, $timeout) { + $scope.chart = null; + this.showGraph = function () { + var config = angular.copy($scope.config); + var firstLoad = true; + config.bindto = "#" + $scope.domid; + var originalXTickCount = null; + if ($scope.data && $scope.config) { + if (!_.isEmpty($scope.data)) { + _.extend(config.data, angular.copy($scope.data)); + } + + config.onresized = function () { + if (this.currentWidth < 400){ + $scope.chart.internal.config.axis_x_tick_culling_max = 3; + } + else if (this.currentWidth < 600){ + $scope.chart.internal.config.axis_x_tick_culling_max = 5; + } + else{ + $scope.chart.internal.config.axis_x_tick_culling_max = originalXTickCount; + } + $scope.chart.flush(); + }; + + + $scope.chart = c3.generate(config); + originalXTickCount = $scope.chart.internal.config.axis_x_tick_culling_max; + $scope.chart.internal.config.onresized.call($scope.chart.internal); + } + + if ($scope.update) { + + $scope.$watch('data', function () { + if (!firstLoad) { + + $scope.chart.load(angular.copy($scope.data), {unload: true}); + if (typeof $scope.data.groups != 'undefined') { + + $scope.chart.groups($scope.data.groups); + } + if (typeof $scope.data.names != 'undefined') { + + $scope.chart.data.names($scope.data.names); + } + $scope.chart.flush(); + } + }, true); + } + $scope.$watch('config.regions', function (newValue, oldValue) { + if (newValue === oldValue) { + return + } + if (typeof $scope.config.regions != 'undefined') { + + $scope.chart.regions($scope.config.regions); + } + }); + + firstLoad = false; + $scope.$watch('resizetrigger', function (newValue, oldValue) { + if (newValue !== oldValue) { + $timeout(function () { + $scope.chart.resize(); + $scope.chart.internal.config.onresized.call($scope.chart.internal); + }); + } + }); + }; + }]) + .directive('c3chart', function ($timeout) { + var chartLinker = function (scope, element, attrs, chartCtrl) { + // Trick to wait for all rendering of the DOM to be finished. + // then we can tell c3js to "connect" to our dom node + $timeout(function () { + chartCtrl.showGraph() + }); + + scope.$on("$destroy", function () { + if (scope.chart !== null) { + scope.chart = scope.chart.destroy(); + delete element; + delete scope.chart; + + } + } + ); + }; + return { + "restrict": "E", + "controller": "ChartCtrl", + "scope": { + "domid": "@domid", + "config": "=config", + "data": "=data", + "resizetrigger": "=resizetrigger", + "update": "=update" + }, + "template": "
", + "replace": true, + "link": chartLinker + } + }); + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.confirmValidate', []). +directive('confirmValidate', [function () { + return { + restrict: 'A', + require: 'ngModel', + link: function ($scope, elem, attrs, ngModel) { + ngModel.$validators.confirm = function (modelValue, viewValue) { + var value = modelValue || viewValue; + + if (value.toLowerCase() == 'confirm') { + return true; + } + return false; + } + } + } +}]) +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.focus', []).directive('focus', function () { + return function (scope, element, attrs) { + attrs.$observe('focus', function (newValue) { + newValue === 'true' && element[0].focus(); + }); + } +}); +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.formErrors', []). +directive('formErrors', function() { + return { + scope: { + errors: '=' + }, + template: '
{{ errorMessage }}
' + } +}) + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.humanFormat', []). + directive('humanFormat', [function () { + /* json inspector */ + return { + restrict: "A", + scope: { + vars: '=', + }, + "link": function (scope, element, attrs) { + scope.$watch('vars', function (newValue, oldValue, scope) { + element.empty(); + element.append(JsonHuman.format(scope.vars)); + }); + + } + } + }]) + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.isoToRelativeTime', []). +directive('isoToRelativeTime', function () { + return { + "restrict": "E", + scope: { + time: '@' + }, + "link": function (scope, element) { + scope.$watch('time', function(newValue, oldValue, scope){ + element.empty(); + element.html(moment.utc(newValue).fromNow()); + }); + } + } +}) +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('ApplicationPermissionsController', ApplicationPermissionsController); + +ApplicationPermissionsController.$inject = ['sectionViewResource', + 'applicationsPropertyResource', 'groupsResource'] + + +function ApplicationPermissionsController(sectionViewResource, applicationsPropertyResource , groupsResource) { + var vm = this; + vm.form = { + autocompleteUser: '', + selectedGroup: null, + selectedUserPermissions: {}, + selectedGroupPermissions: {} + } + vm.possibleGroups = groupsResource.query(null, function(){ + if (vm.possibleGroups.length > 0){ + vm.form.selectedGroup = vm.possibleGroups[0].id; + } + }); + + vm.possibleUsers = []; + _.each(vm.resource.possible_permissions, function (perm) { + vm.form.selectedUserPermissions[perm] = false; + vm.form.selectedGroupPermissions[perm] = false; + }); + + /** + * Converts the permission list into {user, permission_list objects} + * for rendering in templates + * **/ + var tmpObj = { + user: {}, + group: {} + }; + _.each(vm.currentPermissions, function (perm) { + + if (perm.type == 'user') { + if (typeof tmpObj[perm.type][perm.user_name] === 'undefined') { + tmpObj[perm.type][perm.user_name] = { + self: perm, + permissions: [] + } + } + if (tmpObj[perm.type][perm.user_name].permissions.indexOf(perm.perm_name) === -1) { + tmpObj[perm.type][perm.user_name].permissions.push(perm.perm_name); + } + } + else { + if (typeof tmpObj[perm.type][perm.group_name] === 'undefined') { + tmpObj[perm.type][perm.group_name] = { + self: perm, + permissions: [] + } + } + if (tmpObj[perm.type][perm.group_name].permissions.indexOf(perm.perm_name) === -1) { + tmpObj[perm.type][perm.group_name].permissions.push(perm.perm_name); + } + + } + }); + vm.currentPermissions = { + user: _.values(tmpObj.user), + group: _.values(tmpObj.group), + }; + + + + vm.searchUsers = function (searchPhrase) { + + vm.searchingUsers = true; + return sectionViewResource.query({ + section: 'users_section', + view: 'search_users', + 'user_name': searchPhrase + }).$promise.then(function (data) { + vm.searchingUsers = false; + return _.map(data, function (item) { + return item; + }); + }); + }; + + + vm.setGroupPermission = function(){ + var POSTObj = { + 'group_id': vm.form.selectedGroup, + 'permissions': [] + }; + for (var key in vm.form.selectedGroupPermissions) { + if (vm.form.selectedGroupPermissions[key]) { + POSTObj.permissions.push(key) + } + } + applicationsPropertyResource.save({ + key: 'group_permissions', + resourceId: vm.resource.resource_id + }, POSTObj, + function (data) { + var found_row = false; + _.each(vm.currentPermissions.group, function (perm) { + if (perm.self.group_id == data.group.id) { + perm['permissions'] = data['permissions']; + found_row = true; + } + }); + if (!found_row) { + data.self = data.group; + // normalize data format + data.self.group_id = data.self.id; + vm.currentPermissions.group.push(data); + } + }); + + } + + + vm.setUserPermission = function () { + + var POSTObj = { + 'user_name': vm.form.autocompleteUser, + 'permissions': [] + }; + for (var key in vm.form.selectedUserPermissions) { + if (vm.form.selectedUserPermissions[key]) { + POSTObj.permissions.push(key) + } + } + applicationsPropertyResource.save({ + key: 'user_permissions', + resourceId: vm.resource.resource_id + }, POSTObj, + function (data) { + var found_row = false; + _.each(vm.currentPermissions.user, function (perm) { + if (perm.self.user_name == data['user_name']) { + perm['permissions'] = data['permissions']; + found_row = true; + } + }); + if (!found_row) { + data.self = data; + vm.currentPermissions.user.push(data); + } + }); + } + + vm.removeUserPermission = function (perm_name, curr_perm) { + + + var POSTObj = { + key: 'user_permissions', + user_name: curr_perm.self.user_name, + permissions: [perm_name], + resourceId: vm.resource.resource_id + } + applicationsPropertyResource.delete(POSTObj, function (data) { + _.each(vm.currentPermissions.user, function (perm) { + if (perm.self.user_name == data['user_name']) { + perm['permissions'] = data['permissions'] + } + }); + }); + } + + vm.removeGroupPermission = function (perm_name, curr_perm) { + + var POSTObj = { + key: 'group_permissions', + group_id: curr_perm.self.group_id, + permissions: [perm_name], + resourceId: vm.resource.resource_id + } + applicationsPropertyResource.delete(POSTObj, function (data) { + _.each(vm.currentPermissions.group, function (perm) { + if (perm.self.group_id == data.group.id) { + perm['permissions'] = data['permissions'] + } + }); + }); + } +} + +angular.module('appenlight.directives.permissionsForm',[]) + .directive('permissionsForm', function () { + return { + "restrict": "E", + "controller": "ApplicationPermissionsController", + controllerAs: 'permissions', + bindToController: true, + scope: { + currentPermissions: '=', + possiblePermissions: '=', + resource: '=' + }, + templateUrl: 'templates/directives/permissions.html' + } + }) + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.pluginConfig', []).directive('pluginConfig', function () { + return { + scope: {}, + bindToController: { + resource: '=', + section: '=' + }, + restrict: 'E', + templateUrl: 'templates/directives/plugin_config.html', + controller: PluginConfig, + controllerAs: 'plugin_ctrlr' + }; + + PluginConfig.$inject = ['stateHolder']; + + function PluginConfig(stateHolder) { + var vm = this; + vm.plugins = {}; + vm.inclusions = stateHolder.plugins.inclusions[vm.section]; + } +}); + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.postProcessAction', []).directive('postProcessAction', ['applicationsPropertyResource', function (applicationsPropertyResource) { + return { + scope: {}, + bindToController:{ + action: '=', + resource: '=' + }, + controller:postProcessActionController, + controllerAs:'ctrl', + restrict: 'E', + templateUrl: 'templates/directives/postprocess_action.html' + }; + function postProcessActionController(){ + var vm = this; + + var allOps = { + 'eq': 'Equal', + 'ne': 'Not equal', + 'ge': 'Greater or equal', + 'gt': 'Greater than', + 'le': 'Lesser or equal', + 'lt': 'Lesser than', + 'startswith': 'Starts with', + 'endswith': 'Ends with', + 'contains': 'Contains' + }; + + var fieldOps = {}; + fieldOps['http_status'] = ['eq', 'ne', 'ge', 'le']; + fieldOps['group:priority'] = ['eq', 'ne', 'ge', 'le']; + fieldOps['duration'] = ['ge', 'le']; + fieldOps['url_domain'] = ['eq', 'ne', 'startswith', 'endswith', + 'contains']; + fieldOps['url_path'] = ['eq', 'ne', 'startswith', 'endswith', + 'contains']; + fieldOps['error'] = ['eq', 'ne', 'startswith', 'endswith', + 'contains']; + fieldOps['tags:server_name'] = ['eq', 'ne', 'startswith', 'endswith', + 'contains']; + fieldOps['group:occurences'] = ['eq', 'ne', 'ge', 'le']; + + var possibleFields = { + '__AND__': 'All met (composite rule)', + '__OR__': 'One met (composite rule)', + 'http_status': 'HTTP Status', + 'duration': 'Request duration', + 'group:priority': 'Group -> Priority', + 'url_domain': 'Domain', + 'url_path': 'URL Path', + 'error': 'Error', + 'tags:server_name': 'Tag -> Server name', + 'group:occurences': 'Group -> Occurences' + }; + + vm.ruleDefinitions = { + fieldOps: fieldOps, + allOps: allOps, + possibleFields: possibleFields + }; + + vm.possibleActions = [ + ['1', 'Priority +1'], + ['-1', 'Priority -1'] + ]; + + vm.deleteAction = function (action) { + applicationsPropertyResource.remove({ + pkey: vm.action.pkey, + resourceId: vm.resource.resource_id, + key: 'postprocessing_rules' + }, function () { + vm.resource.postprocessing_rules.splice( + vm.resource.postprocessing_rules.indexOf(action), 1); + }); + }; + + + vm.saveAction = function () { + var params = { + 'pkey': vm.action.pkey, + 'resourceId': vm.resource.resource_id, + key: 'postprocessing_rules' + }; + applicationsPropertyResource.update(params, vm.action, + function (data) { + vm.action.dirty = false; + vm.errors = []; + }, function (response) { + if (response.status == 422) { + var errorDict = angular.fromJson(response.data); + vm.errors = _.values(errorDict); + } + }); + }; + + vm.setDirty = function() { + vm.action.dirty = true; + + }; + } + +}]); + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.recursive', []).directive("recursive", function ($compile) { + return { + restrict: "EACM", + priority: 100000, + compile: function (tElement, tAttr) { + var contents = tElement.contents().remove(); + var compiledContents; + return function (scope, iElement, iAttr) { + if (!compiledContents) { + compiledContents = $compile(contents); + } + iElement.append(compiledContents(scope, function (clone) { + return clone; + })); + }; + } + }; +}); + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.reportAlertAction', []).directive('reportAlertAction', ['userSelfPropertyResource', function (userSelfPropertyResource) { + return { + scope: {}, + bindToController:{ + action: '=', + applications: '=', + possibleChannels: '=', + actions: '=', + ruleDefinitions: '=' + }, + controller:reportAlertActionController, + controllerAs:'ctrl', + restrict: 'E', + templateUrl: 'templates/directives/report_alert_action.html' + }; + function reportAlertActionController(){ + var vm = this; + vm.deleteAction = function (actions, action) { + var get = { + key: 'alert_channels_rules', + pkey: action.pkey + }; + userSelfPropertyResource.remove(get, function (data) { + actions.splice(actions.indexOf(action), 1); + }); + + }; + + vm.bindChannel = function(){ + var post = { + channel_pkey: vm.channelToBind.pkey, + action_pkey: vm.action.pkey + }; + + userSelfPropertyResource.save({key: 'alert_channels_actions_binds'}, post, + function (data) { + vm.action.channels = []; + vm.action.channels = data.channels; + }, function (response) { + if (response.status == 422) { + + } + }); + }; + + vm.unBindChannel = function(channel){ + userSelfPropertyResource.delete({ + key: 'alert_channels_actions_binds', + channel_pkey: channel.pkey, + action_pkey: vm.action.pkey + }, + function (data) { + vm.action.channels = []; + vm.action.channels = data.channels; + }, function (response) { + if (response.status == 422) { + + } + }); + }; + + vm.saveAction = function () { + var params = { + key: 'alert_channels_rules', + pkey: vm.action.pkey + }; + userSelfPropertyResource.update(params, vm.action, + function (data) { + vm.action.dirty = false; + vm.errors = []; + }, function (response) { + if (response.status == 422) { + var errorDict = angular.fromJson(response.data); + vm.errors = _.values(errorDict); + } + }); + }; + + vm.possibleNotifications = [ + ['always', 'Always'], + ['only_first', 'Only New'], + ]; + + vm.possibleChannels = _.filter(vm.possibleChannels, function(c){ + return c.supports_report_alerting } + ); + + if (vm.possibleChannels.length > 0){ + vm.channelToBind = vm.possibleChannels[0]; + } + + vm.setDirty = function() { + vm.action.dirty = true; + + }; + } + +}]); + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.ruleReadOnly', []).directive('ruleReadOnly', ['userSelfPropertyResource', function (userSelfPropertyResource) { + return { + scope: {}, + bindToController:{ + parentObj: '=', + rule: '=', + ruleDefinitions: '=', + parentRule: "=", + config: "=" + }, + restrict: 'E', + templateUrl: 'templates/directives/rule_read_only.html', + controller:RuleController, + controllerAs:'rule_ctrlr' + } + function RuleController(){ + var vm = this; + vm.readOnlyPossibleFields = {}; + var labelPairs = _.pairs(vm.parentObj.config); + _.each(labelPairs, function (entry) { + vm.readOnlyPossibleFields[entry[0]] = entry[1].human_label; + }); + } +}]); + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.rule', []).directive('rule', function () { + return { + scope: {}, + bindToController:{ + parentObj: '=', + rule: '=', + ruleDefinitions: '=', + parentRule: "=", + config: "=" + }, + restrict: 'E', + templateUrl: 'templates/directives/rule.html', + controller:RuleController, + controllerAs:'rule_ctrlr' + }; + function RuleController(){ + var vm = this; + + vm.rule.dirty = false; + vm.oldField = vm.rule.field; + + vm.add = function () { + vm.rule.rules.push( + {op: "eq", field: 'http_status', value: ""} + ); + vm.setDirty(); + }; + + vm.setDirty = function() { + vm.rule.dirty = true; + + if (vm.parentObj){ + + + vm.parentObj.dirty = true; + } + }; + + vm.fieldChange = function () { + var new_is_compound = ['__AND__', '__OR__'].indexOf(vm.rule.field) !== -1; + var old_was_compound = ['__AND__', '__OR__'].indexOf(vm.oldField) !== -1; + + if (!new_is_compound) { + vm.rule.op = vm.ruleDefinitions.fieldOps[vm.rule.field][0]; + } + if ((new_is_compound && !old_was_compound)) { + + delete vm.rule.value; + vm.rule.rules = []; + vm.add(); + } + else if (!new_is_compound && old_was_compound) { + + delete vm.rule.rules; + vm.rule.value = ''; + } + vm.oldField = vm.rule.field; + vm.setDirty(); + }; + + vm.deleteRule = function (parent, rule) { + parent.rules.splice(parent.rules.indexOf(rule), 1); + vm.setDirty(); + } + } +}); + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.smallReportGroupList',[]). +directive('smallReportGroupList', [function () { + return { + restrict: "A", + scope: { + groups: '=', + applications: '=' + }, + templateUrl: 'templates/reports/small_report_group_list.html' + } +}]) +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.smallReportList', []). + directive('smallReportList', [function () { + return { + restrict: "A", + scope: { + reports: '=', + applications: '=' + }, + templateUrl: 'templates/reports/small_report_list.html' + } + }]) +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +'use strict'; + +/* Filters */ + +angular.module('appenlight.filters'). + filter('interpolate', ['version', function (version) { + return function (text) { + return String(text).replace(/\%VERSION\%/mg, version); + } + }]) + .filter('isoToRelativeTime', function () { + return function (input) { + return moment.utc(input).fromNow(); + } + }) + + .filter('round', function () { + return function (input, precision) { + return input.toFixed(precision) + } + }) + + .filter('numberToThousands', function () { + return function (input) { + if (input > 1000000) { + var i = input / 1000000; + return i.toFixed(1).toString() + 'M' + } + else if (input > 1000) { + var i = input / 1000; + return i.toFixed(1).toString() + 'k' + } + else { + return input; + } + } + }) + .filter('getOrdered', function () { + return function (input, filterOn) { + var ordered = {}; + for (var key in input) { + ordered[input[key][filterOn]] = input[key]; + } + return ordered; + }; + }) + .filter('objectToOrderedArray', function(){ + return function(items, field, reverse) { + var filtered = []; + angular.forEach(items, function(item) { + filtered.push(item); + }); + filtered.sort(function (a, b) { + return (a[field] > b[field] ? 1 : -1); + }); + if(reverse) filtered.reverse(); + return filtered; + }; + }) + .filter('apdexValue', function () { + return function (input) { + if (input.apdex >= 95) { + return 'satisfactory'; + } else if (input.apdex >= 80) { + return 'tolerating'; + } else { + return 'frustrating'; + } + }; + }) + .filter('truncate', function(){ + return function (text, length, end) { + if (isNaN(length)) + length = 10; + + if (end === undefined) + end = "..."; + + if (text.length <= length || text.length - end.length <= length) { + return text; + } + else { + return String(text).substring(0, length-end.length) + end; + } + + }; + }) + +; + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight').config(['$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) { + + $urlRouterProvider.otherwise('/ui'); + + $stateProvider.state('logs', { + url: '/ui/logs?resource', + templateUrl: 'templates/logs.html', + controller: 'LogsController as logs' + }); + + $stateProvider.state('front_dashboard', { + url: '/ui', + templateUrl: 'templates/dashboard.html', + controller: 'IndexDashboardController as index' + }); + + $stateProvider.state('report', { + abstract: true, + url: '/ui/report', + templateUrl: 'templates/reports/parent_view.html' + }); + + $stateProvider.state('report.list', { + url: '?start_date&min_duration&max_duration&{view_name:any}&{server_name:any}&resource', + templateUrl: 'templates/reports/list.html', + controller: 'ReportsListController as reports_list' + }); + + $stateProvider.state('report.list_slow', { + url: '/list_slow?start_date&min_duration&max_duration&{view_name:any}&{server_name:any}&resource', + templateUrl: 'templates/reports/list_slow.html', + controller: 'ReportsListSlowController as reports_list' + }); + + $stateProvider.state('report.view_detail', { + url: '/:groupId/:reportId', + templateUrl: 'templates/reports/view.html', + controller: 'ReportsViewController as report' + }); + $stateProvider.state('report.view_group', { + url: '/:groupId', + templateUrl: 'templates/reports/view.html', + controller: 'ReportsViewController as report' + }); + $stateProvider.state('events', { + url: '/ui/events', + templateUrl: 'templates/events.html', + controller: 'EventsController as events' + }); + $stateProvider.state('admin', { + url: '/ui/admin', + templateUrl: 'templates/admin/parent_view.html' + }); + $stateProvider.state('admin.user', { + abstract: true, + url: '/user', + templateUrl: 'templates/admin/users/parent_view.html' + }); + $stateProvider.state('admin.user.list', { + url: '/list', + templateUrl: 'templates/admin/users/users_list.html', + controller: 'AdminUsersController as users' + }); + $stateProvider.state('admin.user.create', { + url: '/create', + templateUrl: 'templates/admin/users/users_create.html', + controller: 'AdminUsersCreateController as user' + }); + $stateProvider.state('admin.user.update', { + url: '/{userId}/update', + templateUrl: 'templates/admin/users/users_create.html', + controller: 'AdminUsersCreateController as user' + }); + + + $stateProvider.state('admin.group', { + abstract: true, + url: '/group', + templateUrl: 'templates/admin/groups/parent_view.html' + }); + $stateProvider.state('admin.group.list', { + url: '/list', + templateUrl: 'templates/admin/groups/groups_list.html', + controller: 'AdminGroupsController as groups' + }); + $stateProvider.state('admin.group.create', { + url: '/create', + templateUrl: 'templates/admin/groups/groups_create.html', + controller: 'AdminGroupsCreateController as group' + }); + $stateProvider.state('admin.group.update', { + url: '/{groupId}/update', + templateUrl: 'templates/admin/groups/groups_create.html', + controller: 'AdminGroupsCreateController as group' + }); + + $stateProvider.state('admin.application', { + abstract: true, + url: '/application', + templateUrl: 'templates/admin/users/parent_view.html' + }); + + $stateProvider.state('admin.application.list', { + url: '/list', + templateUrl: 'templates/admin/applications/applications_list.html', + controller: 'AdminApplicationsListController as applications' + }); + + $stateProvider.state('admin.partitions', { + url: '/partitions', + templateUrl: 'templates/admin/partitions.html', + controller: 'AdminPartitionsController as partitions' + }); + $stateProvider.state('admin.system', { + url: '/system', + templateUrl: 'templates/admin/system.html', + controller: 'AdminSystemController as system' + }); + + $stateProvider.state('admin.configs', { + abstract: true, + url: '/configs', + templateUrl: 'templates/admin/configs/parent_view.html' + }); + + $stateProvider.state('admin.configs.list', { + url: '', + templateUrl: 'templates/admin/configs/edit.html', + controller: 'ConfigsListController as configs' + }); + + $stateProvider.state('user', { + url: '/ui/user', + templateUrl: 'templates/user/parent_view.html' + }); + + $stateProvider.state('user.profile', { + abstract: true, + url: '/profile', + templateUrl: 'templates/user/profile.html' + }); + $stateProvider.state('user.profile.edit', { + url: '', + templateUrl: 'templates/user/profile_edit.html', + controller: 'UserProfileController as profile' + }); + + + $stateProvider.state('user.profile.password', { + url: '/password', + templateUrl: 'templates/user/profile_password.html', + controller: 'UserPasswordController as password' + }); + + $stateProvider.state('user.profile.identities', { + url: '/identities', + templateUrl: 'templates/user/profile_identities.html', + controller: 'UserIdentitiesController as identities' + }); + + $stateProvider.state('user.profile.auth_tokens', { + url: '/auth_tokens', + templateUrl: 'templates/user/auth_tokens.html', + controller: 'UserAuthTokensController as auth_tokens' + }); + + $stateProvider.state('user.alert_channels', { + abstract: true, + url: '/alert_channels', + templateUrl: 'templates/user/alert_channels.html' + }); + + $stateProvider.state('user.alert_channels.list', { + url: '', + templateUrl: 'templates/user/alert_channels_list.html', + controller: 'AlertChannelsController as channels' + }); + + $stateProvider.state('user.alert_channels.email', { + url: '/email', + templateUrl: 'templates/user/alert_channels_email.html', + controller: 'AlertChannelsEmailController as email' + }); + + $stateProvider.state('applications', { + abstract: true, + url: '/ui/applications', + templateUrl: 'templates/applications/parent_view.html' + }); + + $stateProvider.state('applications.list', { + url: '', + templateUrl: 'templates/applications/list.html', + controller: 'ApplicationsListController as applications' + }); + $stateProvider.state('applications.update', { + url: '/{resourceId}/update', + templateUrl: 'templates/applications/applications_update.html', + controller: 'ApplicationsUpdateController as application' + }); + + $stateProvider.state('applications.integrations', { + url: '/{resourceId}/integrations', + templateUrl: 'templates/applications/integrations.html', + controller: 'IntegrationsListController as integrations', + data: { + resource: null + } + }); + + $stateProvider.state('applications.purge_logs', { + url: '/purge_logs', + templateUrl: 'templates/applications/applications_purge_logs.html', + controller: 'ApplicationsPurgeLogsController as applications_purge' + }); + + $stateProvider.state('applications.integrations.edit', { + url: '/{integration}', + templateUrl: function ($stateParams) { + return 'templates/applications/integrations/' + $stateParams.integration + '.html' + }, + controller: 'IntegrationController as integration' + }); + + $stateProvider.state('tests', { + url: '/ui/tests', + templateUrl: 'templates/user/alert_channels_test.html', + controller: 'AlertChannelsTestController as test_action' + }); + +}]); + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.services.chartResultParser',[]).factory('chartResultParser', function () { + + function transform(data) { + + /** transform result to a format that is more friendly + * to c3js we don't want to export this way as default + * as TSV stuff is less readable overall + * + * we want format of: + * {x: [unix_timestamps], + * key1: [val,list], + * key2: [val,list]...} + * + * OR + * + * handle special case where we want pie/donut for + * aggregation with a single metric, we need to transform + * the data from: + * [y:list, categories:[cat1,cat2,...]] + * to + * [cat1: val, cat2:val...] format to render properly + */ + var chartC3Config = { + data: { + json: [], + type: 'bar' + }, + point: { + show: false + }, + tooltip: { + format: { + title: function (d) { + if (d) { + return '' + d; + } + return ''; + }, + value: function (value, ratio, id, index) { + return d3.round(value, 3); + } + } + }, + regions: data.rect_regions + }; + var labels = _.keys(data.system_labels); + var specialCases = ['pie', 'donut', 'gauge']; + if (labels.length === 1 && _.contains(specialCases, + data.chart_type.type)) { + var transformedData = {}; + + _.each(data.series, function (item) { + transformedData[item['key']] = item[labels[0]]; + }); + } + else { + var transformedData = {'key': []}; + + _.each(labels, function (label) { + transformedData[label] = []; + }); + + _.each(data.series, function (item) { + for (key in item) { + transformedData[key].push(item[key]) + } + }); + } + + + if (data.parent_agg.type === 'time_histogram') { + chartC3Config.axis = { + x: { + type: 'timeseries', + tick: { + format: '%Y-%m-%d' + } + } + }; + chartC3Config.data.xFormat = '%Y-%m-%dT%H:%M:%S'; + } + else if (data.categories) { + chartC3Config.axis = { + x: { + type: 'category', + categories: data.categories + } + }; + // we don't want to show key as label if it is being + // used as a category instead + if (data.categories) { + delete transformedData['key']; + } + } + + var human_labels = {}; + _.each(_.pairs(data.system_labels), function(entry){ + human_labels[entry[0]] = entry[1].human_label; + }); + var chartC3Data = { + json: transformedData, + names: human_labels, + groups: data.groups, + type: data.chart_type.type + }; + + if (data.parent_agg.type == 'time_histogram') { + chartC3Data.x = 'key'; + } + return {chartC3Data: chartC3Data, chartC3Config: chartC3Config} + } + + return transform +}); + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +var DEFAULT_ACTIONS = { + 'get': {method: 'GET', timeout: 60000 * 2}, + 'save': {method: 'POST', timeout: 60000 * 2}, + 'query': {method: 'GET', isArray: true, timeout: 60000 * 2}, + 'remove': {method: 'DELETE', timeout: 30000}, + 'update': {method: 'PATCH', timeout: 30000}, + 'delete': {method: 'DELETE', timeout: 30000} +}; + +angular.module('appenlight.services.resources', []).factory('usersResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.users, {userId: '@id'}, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('userResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.user, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('usersPropertyResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.usersProperty, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('userSelfResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.userSelf, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('userSelfPropertyResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.userSelfProperty, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('logsNoIdResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.logsNoId, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('reportsResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.reports, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('slowReportsResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.slowReports, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('reportGroupResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.reportGroup, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('reportGroupPropertyResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.reportGroupProperty, null, angular.copy(DEFAULT_ACTIONS)); +}]); + + +angular.module('appenlight.services.resources').factory('reportResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.reports, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('analyticsResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.analyticsAction, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('reportsResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.reports, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('integrationResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.integrationAction, null, angular.copy(DEFAULT_ACTIONS)); +}]); + + +angular.module('appenlight.services.resources').factory('adminResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.adminAction, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('applicationsNoIdResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.applicationsNoId, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('applicationsPropertyResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.applicationsProperty, null, angular.copy(DEFAULT_ACTIONS)); +}]); +angular.module('appenlight.services.resources').factory('applicationsResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.applications, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('sectionViewResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.sectionView, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('groupsNoIdResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.groupsNoId, null, angular.copy(DEFAULT_ACTIONS)); +}]); + + +angular.module('appenlight.services.resources').factory('groupsResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.groups, {userId: '@id'}, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('groupsPropertyResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.groupsProperty, null, angular.copy(DEFAULT_ACTIONS)); +}]); + + +angular.module('appenlight.services.resources').factory('eventsNoIdResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.eventsNoId, null, angular.copy(DEFAULT_ACTIONS)); +}]); + + +angular.module('appenlight.services.resources').factory('eventsResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.events, {userId: '@id'}, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('eventsPropertyResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.eventsProperty, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('configsNoIdResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.configsNoId, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('configsResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.configs, { + key: '@key', + section: '@section' + }, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('pluginConfigsResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.pluginConfigs, { + id: '@id', + plugin_name: '@plugin_name' + }, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('resourcesPropertyResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.resourceProperty, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.services.stateHolder', []).factory('stateHolder', ['$timeout', 'AeConfig', function ($timeout, AeConfig) { + /** + * Holds some common stuff like flash messages, but important part is + * plugins property that is a registry that holds all information about + * loaded plugins, its mutated via .run() functions on inclusion + * @type {{list: Array, timeout: null, extend: flashMessages.extend, pop: flashMessages.pop, cancelTimeout: flashMessages.cancelTimeout, removeMessages: flashMessages.removeMessages}} + */ + var flashMessages = { + list: [], + timeout: null, + extend: function (values) { + + if (this.list.length > 2) { + this.list.splice(0, this.list.length - 2); + } + this.list.push.apply(this.list, values); + this.cancelTimeout(); + this.removeMessages(); + }, + pop: function () { + + this.list.pop(); + }, + cancelTimeout: function () { + if (this.timeout) { + $timeout.cancel(this.timeout); + } + }, + removeMessages: function () { + var self = this; + this.timeout = $timeout(function () { + while (self.list.length > 0) { + self.list.pop(); + } + }, 10000); + } + }; + flashMessages.closeAlert = angular.bind(flashMessages, function (index) { + this.list.splice(index, 1); + this.cancelTimeout(); + }); + /* add flash messages from template generated on non-xhr request level */ + try { + if (AeConfig.flashMessages.length > 0) { + flashMessages.list = AeConfig.flashMessages; + } + } + catch (exc) { + + } + + var Plugins = { + enabled: [], + configs: {}, + inclusions: {}, + addInclusion: function (name, inclusion) { + var self = this; + if (self.inclusions.hasOwnProperty(name) === false) { + self.inclusions[name] = []; + } + self.inclusions[name].push(inclusion); + } + } + + var stateHolder = { + section: 'settings', + resource: null, + plugins: Plugins, + flashMessages: flashMessages, + }; + return stateHolder; +}]); + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.services.typeAheadTagHelper', []).factory('typeAheadTagHelper', function () { + var typeAheadTagHelper = {tags: []}; + typeAheadTagHelper.aheadFilter = function (item, viewValue) { + //dont show "deeper" autocomplete like level:foo with exception of application ones + var label_text = item.text || item; + if (label_text.charAt(label_text.length - 1) != ':' && viewValue.indexOf(':') === -1 && label_text.indexOf('resource:') !== 0) { + return false; + } + if (viewValue.length > 2) { + // with apps we need to do it differently + if (viewValue.toLowerCase().indexOf('resource:') == 0) { + viewValue = viewValue.split(':').pop(); + } + // check if tags match + if (label_text.toLowerCase().indexOf(viewValue.toLowerCase()) === -1) { + return false; + } + } + return true; + }; + typeAheadTagHelper.removeSearchTag = function (tag) { + + var indexValue = _.indexOf(typeAheadTagHelper.tags, tag); + typeAheadTagHelper.tags.splice(indexValue, 1); + + }; + typeAheadTagHelper.addSearchTag = function (tag) { + // do not allow dupes - angular will complain + var found = _.find(typeAheadTagHelper.tags, function (existingTag) { + return existingTag.type == tag.type && existingTag.value == tag.value + }); + if (!found) { + typeAheadTagHelper.tags.push(tag); + } + }; + + return typeAheadTagHelper; +}); + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.services.UUIDProvider', []).factory('UUIDProvider', function () { + var provider = { + genUUID4: function () { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( + /[xy]/g, function (c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : r & 0x3 | 0x8; + return v.toString(16); + } + ); + } + }; + return provider; +}); +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +var underscore = angular.module('underscore', []); +underscore.factory('_', function () { + return window._; // assumes underscore has already been loaded on the page +}); + +;// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +var aeuser = angular.module('appenlight.user', []); +aeuser.factory('AeUser', ['AeConfig', function () { + var decodedAeUser = decodeEncodedJSON(window.AE.user); + + var AeUser = { + user_name: decodedAeUser.user_name || null, + id: decodedAeUser.id, + assigned_reports: decodedAeUser.assigned_reports || null, + latest_events: decodedAeUser.latest_events || null, + permissions: decodedAeUser.permissions || null, + groups: decodedAeUser.groups || null, + applications: [], + dashboards: [] + }; + + AeUser.applications_map = {}; + AeUser.dashboards_map = {}; + AeUser.addApplication = function (item) { + AeUser.applications.push(item); + AeUser.applications_map[item.resource_id] = item; + }; + AeUser.addDashboard = function (item) { + AeUser.dashboards.push(item); + AeUser.dashboards_map[item.resource_id] = item; + }; + + AeUser.removeApplicationById = function (applicationId) { + AeUser.applications = _.filter(AeUser.applications, function (item) { + return item.resource_id != applicationId; + }); + delete AeUser.applications_map[applicationId]; + }; + AeUser.removeDashboardById = function (dashboardId) { + AeUser.dashboards = _.filter(AeUser.dashboards, function (item) { + return item.resource_id != dashboardId; + }); + delete AeUser.dashboards_map[dashboardId]; + }; + + AeUser.hasAppPermission = function (perm_name) { + if (AeUser.permissions.indexOf('root_administration') !== -1) { + return true + } + return AeUser.permissions.indexOf(perm_name) !== -1; + }; + + AeUser.hasContextPermission = function (permName, ACLList) { + var hasPerm = false; + _.each(ACLList, function (ACL) { + // is this the right perm? + if (ACL.perm_name == permName || + ACL.perm_name == '__all_permissions__') { + // perm for this user or a group user belongs to + if (ACL.user_name === AeUser.user_name || + AeUser.groups.indexOf(ACL.group_name) !== -1) { + hasPerm = true + } + } + }); + + return hasPerm; + }; + + _.each(decodedAeUser.applications, function (item) { + AeUser.addApplication(item); + }); + _.each(decodedAeUser.dashboards, function (item) { + AeUser.addDashboard(item); + }); + + return AeUser; +}]); diff --git a/backend/src/appenlight/subscribers.py b/backend/src/appenlight/subscribers.py new file mode 100644 index 0000000..767cef7 --- /dev/null +++ b/backend/src/appenlight/subscribers.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import hashlib +import os + +from pyramid.i18n import TranslationStringFactory +from pyramid import threadlocal + +_ = TranslationStringFactory('pyramid') + +from appenlight import security +from appenlight.lib import helpers, generate_random_string +from appenlight.models.services.config import ConfigService + + +def gen_urls(request): + urls = { + 'baseUrl': request.route_url('/'), + 'applicationsNoId': request.route_url('applications_no_id'), + 'applications': request.route_url('applications', resource_id='REPLACE_ID').replace('REPLACE_ID',':resourceId'), + 'applicationsProperty': request.route_url('applications_property',key='REPLACE_KEY', resource_id='REPLACE_ID').replace('REPLACE_ID',':resourceId').replace('REPLACE_KEY',':key'), + 'configsNoId': request.route_url('admin_configs'), + 'configs': request.route_url('admin_config', key='REPLACE_KEY', section='REPLACE_SECTION').replace('REPLACE_SECTION',':section').replace('REPLACE_KEY',':key'), + 'docs': 'http://getappenlight.com/page/api/main.html', + 'eventsNoId': request.route_url('events_no_id'), + 'events': request.route_url('events', event_id='REPLACE_ID').replace('REPLACE_ID',':eventId'), + 'eventsProperty': request.route_url('events_property',key='REPLACE_KEY', event_id='REPLACE_ID').replace('REPLACE_ID',':eventId').replace('REPLACE_KEY',':key'), + 'groupsNoId': request.route_url('groups_no_id'), + 'groups': request.route_url('groups', group_id='REPLACE_ID').replace('REPLACE_ID',':groupId'), + 'groupsProperty': request.route_url('groups_property',key='REPLACE_KEY', group_id='REPLACE_ID').replace('REPLACE_ID',':groupId').replace('REPLACE_KEY',':key'), + 'logsNoId': request.route_url('logs_no_id'), + 'integrationAction': request.route_url('integrations_id',action='REPLACE_ACT', resource_id='REPLACE_RID', integration='REPLACE_IID').replace('REPLACE_RID',':resourceId').replace('REPLACE_ACT',':action').replace('REPLACE_IID',':integration'), + 'usersNoId': request.route_url('users_no_id'), + 'users': request.route_url('users', user_id='REPLACE_ID').replace('REPLACE_ID',':userId'), + 'usersProperty': request.route_url('users_property',key='REPLACE_KEY', user_id='REPLACE_ID').replace('REPLACE_ID',':userId').replace('REPLACE_KEY',':key'), + 'userSelf': request.route_url('users_self'), + 'userSelfProperty': request.route_url('users_self_property',key='REPLACE_KEY').replace('REPLACE_KEY',':key'), + 'reports': request.route_url('reports'), + 'reportGroup': request.route_url('report_groups', group_id='REPLACE_RID').replace('REPLACE_RID',':groupId'), + 'reportGroupProperty': request.route_url('report_groups_property', key='REPLACE_KEY', group_id='REPLACE_GID').replace('REPLACE_KEY',':key').replace('REPLACE_GID',':groupId'), + 'pluginConfigsNoId': request.route_url('plugin_configs', plugin_name='REPLACE_TYPE').replace('REPLACE_TYPE',':plugin_name'), + 'pluginConfigs': request.route_url('plugin_config', id='REPLACE_ID', plugin_name='REPLACE_TYPE').replace('REPLACE_ID',':id').replace('REPLACE_TYPE',':plugin_name'), + 'resourceProperty': request.route_url('resources_property',key='REPLACE_KEY', resource_id='REPLACE_ID').replace('REPLACE_ID',':resourceId').replace('REPLACE_KEY',':key'), + 'slowReports': request.route_url('slow_reports'), + 'sectionView': request.route_url('section_view', section='REPLACE_S', view='REPLACE_V').replace('REPLACE_S',':section').replace('REPLACE_V',':view'), + 'otherRoutes': { + 'register': request.route_url('register') , + 'lostPassword': request.route_url('lost_password') , + 'lostPasswordGenerate': request.route_url('lost_password_generate') + }, + 'social_auth': { + 'google':request.route_url('social_auth', provider='google'), + 'twitter':request.route_url('social_auth', provider='twitter'), + 'bitbucket':request.route_url('social_auth', provider='bitbucket'), + 'github':request.route_url('social_auth', provider='github'), + }, + "plugins":{}, + "adminAction": request.route_url('admin', action="REPLACE_ACT").replace('REPLACE_ACT',':action') + } + return urls + +def new_request(event): + environ = event.request.environ + event.request.response.headers['X-Frame-Options'] = 'SAMEORIGIN' + event.request.response.headers['X-XSS-Protection'] = '1; mode=block' + # can this be enabled on non https deployments? + # event.request.response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubdomains;' + + # do not send XSRF token with /api calls + if not event.request.path.startswith('/api'): + if environ['wsgi.url_scheme'] == 'https': + event.request.response.set_cookie( + 'XSRF-TOKEN', event.request.session.get_csrf_token(), + secure=True) + else: + event.request.response.set_cookie( + 'XSRF-TOKEN', event.request.session.get_csrf_token()) + if event.request.user: + event.request.response.headers[ + 'x-appenlight-uid'] = '%s' % event.request.user.id + + +def add_renderer_globals(event): + request = event.get("request") or threadlocal.get_current_request() + + renderer_globals = event + renderer_globals["h"] = helpers + renderer_globals["js_hash"] = request.registry.js_hash + renderer_globals["css_hash"] = request.registry.css_hash + renderer_globals['_'] = _ + renderer_globals['security'] = security + renderer_globals['flash_msgs'] = [] + renderer_globals['js_plugins'] = [] + renderer_globals['top_nav'] = { + 'menu_dashboards_items': [], + 'menu_reports_items': [], + 'menu_logs_items': [], + 'menu_settings_items': [], + 'menu_admin_items': [], + } + + if 'jinja' in event['renderer_info'].type: + renderer_globals['url_list'] = gen_urls(request) + # add footer html and some other global vars to renderer + for module, config in request.registry.appenlight_plugins.items(): + if config['url_gen']: + urls = config['url_gen'](request) + renderer_globals['url_list']['plugins'][module] = urls + + if config['javascript']: + renderer_globals['js_plugins'].append( + ({'name':module, 'config':config['javascript']})) + for nav_key in renderer_globals['top_nav'].keys(): + if nav_key in config['top_nav'] and config['top_nav'][nav_key]: + renderer_globals['top_nav'][nav_key].append( + config['top_nav'][nav_key]) + + if request.has_permission('root_administration', + security.RootFactory(request)): + renderer_globals['top_nav']['menu_admin_items'].append( + {'sref': 'admin', 'label': 'Admin Settings'} + ) + + footer_config = ConfigService.by_key_and_section( + 'template_footer_html', 'global', default_value='') + + renderer_globals['template_footer_html'] = footer_config.value + try: + renderer_globals['root_administrator'] = request.has_permission( + 'root_administration', security.RootFactory(request)) + except AttributeError: + renderer_globals['root_administrator'] = False + + renderer_globals['_mail_url'] = request.registry.settings['_mail_url'] + + if not request: + return + + # do not sens flash headers with /api calls + if not request.path.startswith('/api'): + flash_msgs = helpers.get_type_formatted_flash(request) + renderer_globals['flash_msgs'] = flash_msgs + request.add_flash_to_headers() + +def application_created(app): + webassets_dir = app.app.registry.settings.get('webassets.dir') + js_hash = generate_random_string() + css_hash = generate_random_string() + if webassets_dir: + js_hasher = hashlib.md5() + css_hasher = hashlib.md5() + for root, dirs, files in os.walk(webassets_dir): + for name in files: + filename = os.path.join(root, name) + if name.endswith('css'): + with open(filename, 'r', encoding='utf8', + errors='replace') as f: + for line in f: + css_hasher.update(line.encode('utf8')) + elif name.endswith('js'): + with open(filename, 'r', encoding='utf8', + errors='replace') as f: + for line in f: + js_hasher.update(line.encode('utf8')) + js_hash = js_hasher.hexdigest() + css_hash = css_hasher.hexdigest() + app.app.registry.js_hash = js_hash + app.app.registry.css_hash = css_hash diff --git a/backend/src/appenlight/templates/dashboard/index.jinja2 b/backend/src/appenlight/templates/dashboard/index.jinja2 new file mode 100644 index 0000000..ccae003 --- /dev/null +++ b/backend/src/appenlight/templates/dashboard/index.jinja2 @@ -0,0 +1,9 @@ +{% extends "/layout.jinja2" %} + +{% block ng_view %}ui-view{% endblock %} + +{% block page_title %} +

{{ _('Dashboard') }}

+{% endblock %} + +{% block content%}{% endblock %} diff --git a/backend/src/appenlight/templates/email_templates/alert_chart.jinja2 b/backend/src/appenlight/templates/email_templates/alert_chart.jinja2 new file mode 100644 index 0000000..2a61fcc --- /dev/null +++ b/backend/src/appenlight/templates/email_templates/alert_chart.jinja2 @@ -0,0 +1,19 @@ +{% extends "/email_templates/layout.jinja2" %} +{% block page_title %}{% endblock %} +{% block content %} +

CHART: Alert for dashboard {{resource.resource_name}} and chart "{{ chart_name }}"

+ +

Rule "{{ action_name }}" got matched for following values:

+ +
    + {% for item in readable_values %} +
  • {{ item['label'] }}: {{ item['value'] }}
  • + {% endfor %} +
+ + {{ destination_url }} + +

Status change on {{ since_when.strftime('%Y-%m-%d %H:%M') }}

+ + +{% endblock %} diff --git a/backend/src/appenlight/templates/email_templates/alert_reports.jinja2 b/backend/src/appenlight/templates/email_templates/alert_reports.jinja2 new file mode 100644 index 0000000..c6636c1 --- /dev/null +++ b/backend/src/appenlight/templates/email_templates/alert_reports.jinja2 @@ -0,0 +1,11 @@ +{% extends "/email_templates/layout.jinja2" %} +{% block page_title %}{% endblock %} +{% block content %} +

{{ alert_action }} : {{ alert_type | replace('alert', '')| replace('_', ' ') | upper}}Alert for {{resource.resource_name}}

+

Status change on {{ since_when.strftime('%Y-%m-%d %H:%M') }}

+ {% if event_values %} +

Your app sent at least {{ event_values['reports'] }} reports per minute.

+ {% endif %} + + {{ destination_url }} +{% endblock %} diff --git a/backend/src/appenlight/templates/email_templates/alert_uptime.jinja2 b/backend/src/appenlight/templates/email_templates/alert_uptime.jinja2 new file mode 100644 index 0000000..457a17a --- /dev/null +++ b/backend/src/appenlight/templates/email_templates/alert_uptime.jinja2 @@ -0,0 +1,8 @@ +{% extends "/email_templates/layout.jinja2" %} +{% block page_title %}{% endblock %} +{% block content %} +

UPTIME: Alert for {{resource.resource_name}}

+

Status change on {{ since_when.strftime('%Y-%m-%d %H:%M') }}

+ {{ reason }} +

+{% endblock %} diff --git a/backend/src/appenlight/templates/email_templates/assigned_report.jinja2 b/backend/src/appenlight/templates/email_templates/assigned_report.jinja2 new file mode 100644 index 0000000..721aa36 --- /dev/null +++ b/backend/src/appenlight/templates/email_templates/assigned_report.jinja2 @@ -0,0 +1,22 @@ +{% extends "/email_templates/layout.jinja2" %} +{% block page_title %}{{_('You just got a report assigned for review')}}{% endblock %} +{% block content %} +

You just got a report assigned for review

+

{{application.title}}

+

+ + You can view it by clicking following link + +

Report details

+
    +
  • Error: {{report_group.get_report().error}}
  • +
  • Last occurred: {{report_group.last_timestamp.strftime('%Y-%m-%d %H:%M')}}
  • +
  • Server: {{report_group.get_report().server_name}}
  • +
  • Occurences: {{report_group.occurences}}
  • +
  • HTTP status: {{report_group.get_report().http_status}}
  • +
  • URL Path: {{report_group.get_report().url_path}}
  • +
+

+ + +{% endblock %} \ No newline at end of file diff --git a/backend/src/appenlight/templates/email_templates/authorize_email.jinja2 b/backend/src/appenlight/templates/email_templates/authorize_email.jinja2 new file mode 100644 index 0000000..d23c3da --- /dev/null +++ b/backend/src/appenlight/templates/email_templates/authorize_email.jinja2 @@ -0,0 +1,15 @@ +{% extends "/email_templates/layout.jinja2" %} +{% block page_title %}{% endblock %} +{% block content %} +

We received a request to authorize this email in {{ _mail_url }} service.

+

Please confirm this email by clicking following url:

+

+ {{request.route_url('section_view', section='user_section',view='alert_channels_authorize', + _query=(('security_code',security_code,),('channel_name','email',),), + _app_url=_mail_url) + }} +

+

If you didn't want to authorize this channel please ignore this email.

+{% endblock %} \ No newline at end of file diff --git a/backend/src/appenlight/templates/email_templates/contact.jinja2 b/backend/src/appenlight/templates/email_templates/contact.jinja2 new file mode 100644 index 0000000..c3ce587 --- /dev/null +++ b/backend/src/appenlight/templates/email_templates/contact.jinja2 @@ -0,0 +1,24 @@ +{% extends "/email_templates/layout.jinja2" %} +{% block page_title %}{{_('Password renewal request!')}}{% endblock %} +{% block content %} +

Contact Form

+ +

Email

+
{{form.email.data}}
+ +

Title

+
{{form.title.data}}
+ +

Message

+
{{form.message.data}}
+ +

Company

+
{{form.company.data}}
+ +

Name

+
{{form.first_name.data}} {{form.last_name.data}}
+ +

IP

+
{{request.environ.get('REMOTE_ADDR')}}
+ +{% endblock %} \ No newline at end of file diff --git a/backend/src/appenlight/templates/email_templates/layout.jinja2 b/backend/src/appenlight/templates/email_templates/layout.jinja2 new file mode 100644 index 0000000..c41ad3c --- /dev/null +++ b/backend/src/appenlight/templates/email_templates/layout.jinja2 @@ -0,0 +1,106 @@ + + + + +App Enlight :: {% block page_title %}{% endblock %} + + + + + + + Your Message Subject or Title + + + + + + + + + + + + + + + + + + + + + + + +
+ +

+ + App Enlight

+
+
+ {% if user %} +

Hello {% if user.first_name and user.first_name|length > 0 %}{{user.first_name}} {{user.last_name}} + {% else %}{{user.user_name}}{% endif %}

+ {% endif %} + + {% block content %}{% endblock %} +
+

You are receiving this message because you are a member of + + {{ _mail_url }}.

+

You can change your notification status in settings section after you sign to your account.

+

RhodeCode / App Enlight - RhodeCode, Inc. 201 Spear Street, Suite 1100 San Francisco, CA 94105, United States

+
+ + diff --git a/backend/src/appenlight/templates/email_templates/lost_password.jinja2 b/backend/src/appenlight/templates/email_templates/lost_password.jinja2 new file mode 100644 index 0000000..fc373cb --- /dev/null +++ b/backend/src/appenlight/templates/email_templates/lost_password.jinja2 @@ -0,0 +1,13 @@ +{% extends "/email_templates/layout.jinja2" %} +{% block page_title %}{{ _('Password renewal request!') }}{% endblock %} +{% block content %} +

We received a request to create a new password for your account.

+

You can generate it by clicking following URL:

+

+ + {{ request.route_url('lost_password_generate', _query=(('security_code',user.security_code,),('user_name',user.user_name,)),_app_url=_mail_url) }} + +

+

This link expires in 10 minutes from generation time.

+

If you didn't request new password please ignore this email.

+{% endblock %} \ No newline at end of file diff --git a/backend/src/appenlight/templates/email_templates/new_comment_report.jinja2 b/backend/src/appenlight/templates/email_templates/new_comment_report.jinja2 new file mode 100644 index 0000000..10f7fb6 --- /dev/null +++ b/backend/src/appenlight/templates/email_templates/new_comment_report.jinja2 @@ -0,0 +1,26 @@ +{% extends "/email_templates/layout.jinja2" %} +{% block page_title %}{{_('You just got a report assigned for review')}}{% endblock %} +{% block content %} +

{{commenting_user.user_name}} added new comment.

+

{{application.title}}

+

+ + You can view it by clicking following link + + + +

+    {{comment.processed_body}}
+    
+ +

Report details

+
    +
  • Error: {{report_group.error}}
  • +
  • Commented on: {{comment.created_timestamp.strftime('%Y-%m-%d %H:%M')}}
  • +
  • Occurences: {{report_group.occurences}}
  • +
  • HTTP status: {{report_group.http_status}}
  • +
  • URL Path: {{report_group.url_path}}
  • +
+

+ +{% endblock %} diff --git a/backend/src/appenlight/templates/email_templates/notify_reports.jinja2 b/backend/src/appenlight/templates/email_templates/notify_reports.jinja2 new file mode 100644 index 0000000..9291518 --- /dev/null +++ b/backend/src/appenlight/templates/email_templates/notify_reports.jinja2 @@ -0,0 +1,35 @@ +{% extends "/email_templates/layout.jinja2" %} +{% block page_title %}{% endblock %} +{% block email_width %}98%{% endblock %}; +{% block email_max_width %}98%{% endblock %}; +{% block content %} +

Recent reports since: {{timestamp}}

+

+

{{resource_name}}

+ + + + + + + + + + + {% for occurences, report in reports %} + + + + + + + {% endfor %} + +
Time#ErrorLocation
{{report.last_timestamp.strftime('%Y-%m-%d %H:%M')}}
+ @{{report.get_report().tags.get('server_name')}}
{{occurences}} + + {{report.error or 'Slow Report'}} + + {{report.get_report().tags.get('view_name') or report.get_report().url_path}}
+

+{% endblock %} diff --git a/backend/src/appenlight/templates/email_templates/registered.jinja2 b/backend/src/appenlight/templates/email_templates/registered.jinja2 new file mode 100644 index 0000000..b4266a2 --- /dev/null +++ b/backend/src/appenlight/templates/email_templates/registered.jinja2 @@ -0,0 +1,50 @@ +{% extends "/email_templates/layout.jinja2" %} +{% block page_title %}{{_('You have just registered. Welcome!')}}{% endblock %} +{% block content %} +

Now that you are member of App Enlight you can enjoy features of our service:

+
    +
  • Error collection and analysys
  • +
  • Performance monitoring
  • +
  • Log aggregation
  • +
  • Uptime monitoring
  • +
  • Alerts
  • +
+ +

{{_('App Enlight quickstart')}}

+

1 Create application

+

+ For App Enlight to operate you need to + create application profile that allows you to + obtain API key that one of the clients can use. +

+
+ +

2 Integrate our client

+

+ In order for your application to stream meaningful information you will need to + integrate a compatible client for your language of choice. +

+ +

Head over to developers section for information on currently available + clients that you can plug into your software

+ +

In case of any issues or questions you might be having with integration, + feel free to contact us: info@appenlight.com

+ +

3 Add alert channels

+

+ It is a good idea to configure an + + email alert channel that you can use to receive + notifications about events that happen in your application. +

+ +

+ It can be the same email account you used to register with App Enlight - + although we often recommend using separate errors@... account + designated for alert notifications. +

+
+ + +{% endblock %} diff --git a/backend/src/appenlight/templates/error.jinja2 b/backend/src/appenlight/templates/error.jinja2 new file mode 100644 index 0000000..0a3ca90 --- /dev/null +++ b/backend/src/appenlight/templates/error.jinja2 @@ -0,0 +1,12 @@ +{% extends "/layout.jinja2" %} +{% set layout_disable_menu = True %} +{% block content %} +
+ + + + +

500: OMG!!! Internal Server Error

+
+{% endblock %} +{% block section_name %}errorPage{% endblock %} \ No newline at end of file diff --git a/backend/src/appenlight/templates/footer.jinja2 b/backend/src/appenlight/templates/footer.jinja2 new file mode 100644 index 0000000..8b594f7 --- /dev/null +++ b/backend/src/appenlight/templates/footer.jinja2 @@ -0,0 +1,9 @@ + diff --git a/backend/src/appenlight/templates/forbidden.jinja2 b/backend/src/appenlight/templates/forbidden.jinja2 new file mode 100644 index 0000000..ca11d87 --- /dev/null +++ b/backend/src/appenlight/templates/forbidden.jinja2 @@ -0,0 +1,29 @@ +{% extends "/layout.jinja2" %} +{% set layout_disable_menu = True %} +{% block page_title %}

403: Forbidden

{% endblock %} +{% block content %} +
+ + + +{% if csrf %} +

Your CSRF token has expired

+

Wikipedia CSRF article

+{% else %} +

Are you authorized to see this? We don't think so.

+{% endif %} + +{% if not request.user %} +

+

+{{widgets.render_form(sign_in_form)}} +
+ +{% endif%} +

+
+{% endblock %} + +{% block menu %}{% endblock %} +{% block section_name %}forbiddenPage{% endblock %} diff --git a/backend/src/appenlight/templates/header.jinja2 b/backend/src/appenlight/templates/header.jinja2 new file mode 100644 index 0000000..c15fddd --- /dev/null +++ b/backend/src/appenlight/templates/header.jinja2 @@ -0,0 +1,114 @@ + + diff --git a/backend/src/appenlight/templates/ini/production.ini.jinja2 b/backend/src/appenlight/templates/ini/production.ini.jinja2 new file mode 100644 index 0000000..67800de --- /dev/null +++ b/backend/src/appenlight/templates/ini/production.ini.jinja2 @@ -0,0 +1,176 @@ +[app:appenlight] +use = egg:appenlight +reload_templates = false +debug_authorization = false +debug_notfound = false +debug_routematch = false +debug_templates = false +default_locale_name = en +sqlalchemy.url = {{ appenlight_dbstring }} +sqlalchemy.pool_size = 10 +sqlalchemy.max_overflow = 50 +sqlalchemy.echo = false +jinja2.directories = appenlight:templates +jinja2.filters = nl2br = appenlight.lib.jinja2_filters.nl2br + +#includes +appenlight.includes = + +#redis +redis.url = redis://localhost:6379/0 +redis.redlock.url = redis://localhost:6379/3 + +#elasticsearch +elasticsearch.nodes = http://127.0.0.1:9200 + +#dirs +webassets.dir = %(here)s/webassets/ + +# encryption +encryption_secret = {{appenlight_encryption_secret}} + +#authtkt +# uncomment if you use SSL +# authtkt.secure = true +authtkt.secret = {{appenlight_authtkt_secret}} +# session settings +redis.sessions.secret = {{appenlight_redis_session_secret}} +redis.sessions.timeout = 86400 + +# session cookie settings +redis.sessions.cookie_name = appenlight +redis.sessions.cookie_max_age = 2592000 +redis.sessions.cookie_path = / +redis.sessions.cookie_domain = +# uncomment if you use SSL +redis.sessions.cookie_secure = True +redis.sessions.cookie_httponly = True +redis.sessions.cookie_on_exception = True +redis.sessions.prefix = appenlight:session: + +#cache +cache.regions = default_term, second, short_term, long_term +cache.type = ext:memcached +cache.url = 127.0.0.1:11211 +cache.lock_dir = %(here)s/data/cache/lock +cache.second.expire = 1 +cache.short_term.expire = 60 +cache.default_term.expire = 300 + +#mailing +mailing.app_url = https://{{appenlight_domain}} +mailing.from_name = App Enlight +mailing.from_email = no-reply@{{appenlight_domain}} + +### +# Authomatic configuration +### + +authomatic.secret = +authomatic.pr.facebook.app_id = +authomatic.pr.facebook.secret = +authomatic.pr.twitter.key = +authomatic.pr.twitter.secret = +authomatic.pr.google.key = +authomatic.pr.google.secret = +authomatic.pr.github.key = +authomatic.pr.github.secret = +authomatic.pr.github.scope = +authomatic.pr.bitbucket.key = +authomatic.pr.bitbucket.secret = + +#ziggurat +ziggurat_foundations.model_locations.User = appenlight.models.user:User +ziggurat_foundations.sign_in.username_key = sign_in_user_name +ziggurat_foundations.sign_in.password_key = sign_in_user_password +ziggurat_foundations.sign_in.came_from_key = came_from + +#cometd +cometd.server = http://127.0.0.1:8088 +cometd.secret = secret +cometd.ws_url = wss://{{appenlight_domain}}/channelstream + +# for celery +appenlight.api_key = +appenlight.transport_config = +appenlight.public_api_key = + +# celery +celery.broker_type = redis +celery.broker_url = redis://localhost:6379/3 +celery.concurrency = 8 +celery.timezone = UTC + +[filter:paste_prefix] +use = egg:PasteDeploy#prefix + + +[filter:appenlight_client] +use = egg:appenlight_client +appenlight.api_key = +appenlight.transport_config = +appenlight.report_local_vars = true +appenlight.report_404 = true +appenlight.timing.dbapi2_psycopg2 = 0.3 + + + +[pipeline:main] +pipeline = paste_prefix + appenlight_client + appenlight + + + +[server:main] +use = egg:gunicorn#main +host = 0.0.0.0:6543, unix:/tmp/appenlight.sock +workers = 6 +timeout = 90 +max_requests = 10000 + +[server:api] +use = egg:gunicorn#main +host = 0.0.0.0:6553, unix:/tmp/api.appenlight.sock +workers = 8 +max_requests = 10000 + + +# Begin logging configuration + +[loggers] +keys = root, appenlight, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_appenlight] +level = WARN +handlers = +qualname = appenlight + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/backend/src/appenlight/templates/layout.jinja2 b/backend/src/appenlight/templates/layout.jinja2 new file mode 100644 index 0000000..3dab8a5 --- /dev/null +++ b/backend/src/appenlight/templates/layout.jinja2 @@ -0,0 +1,25 @@ +{% extends "/layout_base.jinja2" %} + +{% block outer_content %} + +
+
+
+ {% raw %} + {{message.msg}} + {% endraw %} +
+
+ +
+
+ + + {% block content %}{% endblock %} + + +
+
+
+ +{% endblock %} diff --git a/backend/src/appenlight/templates/layout_base.jinja2 b/backend/src/appenlight/templates/layout_base.jinja2 new file mode 100644 index 0000000..4c8d564 --- /dev/null +++ b/backend/src/appenlight/templates/layout_base.jinja2 @@ -0,0 +1,96 @@ +{% import 'widgets.jinja2' as widgets %} + + + + {% block title %}Application performance, exception and error monitoring for Python, + Django, Flask and Javascript - App Enlight{% endblock %} + {% block meta %} + + + + + + + + + {% endblock %} + {% block styles %} + + + + {% endblock %} + {% block additional_styles %}{% endblock %} + + + {% block scripts %} + {% block additional_layout_scripts %}{% endblock %} + {% endblock %} + {% if request.registry.settings.get('appenlight.public_api_key') %} + + {% endif %} + + {% for plugin in js_plugins %} + + {% endfor %} + + + +
+
+ + +
+{% include "/header.jinja2" with context %} +
+{% block outer_content %}Content{% endblock %} +
+ +{% include "/footer.jinja2" %} +
+ + + diff --git a/backend/src/appenlight/templates/not_found.jinja2 b/backend/src/appenlight/templates/not_found.jinja2 new file mode 100644 index 0000000..99aa514 --- /dev/null +++ b/backend/src/appenlight/templates/not_found.jinja2 @@ -0,0 +1,16 @@ +{% extends "/layout.jinja2" %} +{% set layout_disable_menu = True %} +{% block page_title %}

404: {{_('This is NOT the page you are looking for.')}}

{% endblock %} +{% block content %} +
+ + + +

+404: Go back +

+
+{% endblock %} + +{% block menu %}{% endblock %} +{% block section_name %} notFoundPage{% endblock %} \ No newline at end of file diff --git a/backend/src/appenlight/templates/tests/alerting.jinja2 b/backend/src/appenlight/templates/tests/alerting.jinja2 new file mode 100644 index 0000000..818c074 --- /dev/null +++ b/backend/src/appenlight/templates/tests/alerting.jinja2 @@ -0,0 +1,49 @@ +{% extends "/layout.jinja2" %} +{% block page_title %} +

{{ _('Alerting') }}

+{% endblock %} + + +{% block content %} + +
+

Alerting test

+ {% for channel in alert_channels %} +
+ {{ channel.channel_visible_value }} :: + Error alert + - + Slow alert + - + Report notification + - + Uptime + - + Digest + + - + Chart alert +
+ {% endfor %} +
+ + +{% endblock %} diff --git a/backend/src/appenlight/templates/tests/jinja2.jinja2 b/backend/src/appenlight/templates/tests/jinja2.jinja2 new file mode 100644 index 0000000..80691f0 --- /dev/null +++ b/backend/src/appenlight/templates/tests/jinja2.jinja2 @@ -0,0 +1,3 @@ +JINJA + +{{sleep(0.1)}} \ No newline at end of file diff --git a/backend/src/appenlight/templates/tests/js_airbrake_error.jinja2 b/backend/src/appenlight/templates/tests/js_airbrake_error.jinja2 new file mode 100644 index 0000000..4be6f26 --- /dev/null +++ b/backend/src/appenlight/templates/tests/js_airbrake_error.jinja2 @@ -0,0 +1,57 @@ +{% extends "/layout.jinja2" %} +{% from '/reports/reports_small_list_old.jinja2' import render_reports with context %} +{% block additional_layout_scripts %} + +{% endblock %} +{% block content_class %}{% endblock %} +{% set layout_disable_menu = True %} + +{% block additional_styles %} + + + +{% endblock %} + +{% block page_title %} +

{{_('JS')}}

+{% endblock %} +{% block content %} + +{% endblock %} diff --git a/backend/src/appenlight/templates/tests/js_error.jinja2 b/backend/src/appenlight/templates/tests/js_error.jinja2 new file mode 100644 index 0000000..6b30e93 --- /dev/null +++ b/backend/src/appenlight/templates/tests/js_error.jinja2 @@ -0,0 +1,53 @@ +{% extends "/layout.jinja2" %} +{% block dojodeps %} + deps: ["appenlight","appenlight/sections/test_error"] +{% endblock %} + +{% block additional_layout_scripts %} + +{% endblock %} +{% block content_class %}{% endblock %} +{% set layout_disable_menu = True %} + +{% block additional_styles %} + +{% endblock %} + +{% block page_title %} +

{{ _('JS') }}

+{% endblock %} +{% block content %} + +{% endblock %} diff --git a/backend/src/appenlight/templates/tests/js_log.jinja2 b/backend/src/appenlight/templates/tests/js_log.jinja2 new file mode 100644 index 0000000..146e5c8 --- /dev/null +++ b/backend/src/appenlight/templates/tests/js_log.jinja2 @@ -0,0 +1,33 @@ +{% extends "/layout.jinja2" %} +{% block additional_layout_scripts %} + +{% endblock %} + +{% block additional_styles %} + +{% endblock %} + +{% block content_class %}{% endblock %} +{% set layout_disable_menu = True %} + + + +{% block page_title %} +

{{_('JS')}}

+{% endblock %} +{% block content %} + + + +{% endblock %} diff --git a/backend/src/appenlight/templates/tests/styling.jinja2 b/backend/src/appenlight/templates/tests/styling.jinja2 new file mode 100644 index 0000000..cb608e7 --- /dev/null +++ b/backend/src/appenlight/templates/tests/styling.jinja2 @@ -0,0 +1,214 @@ +{% extends "/layout.jinja2" %} +{% from '/reports/reports_small_list_old.jinja2' import render_reports with context %} +{% block additional_layout_scripts %} +dojo.require('appenlight.dashboard.index'); +{% endblock %} +{% block content_class %}{% endblock %} +{% set layout_disable_menu = True %} + +{% block page_title %} +

{{_('Dashboard')}}

+{% endblock %} +{% block content %} + +

Header 1

+

Header 2

+

Header 3

+

paragraph paragraph paragraph paragraph paragraph paragraph paragraph

+

paragraph paragraph paragraph paragraph paragraph paragraph paragraph

+

paragraph paragraph paragraph paragraph paragraph paragraph paragraph

+ + +{{_('Dashboard')}} +{{_('Browse Reports')}} +{{_('Browse Logs')}} +{{_('Browse Slow Requests')}} +{{_('Applications')}}{{_('Profile settings')}} + +

API Documentation

+ + + +

Errormator provides, simple yet powerful API that allows +your applications to interact with it. Basic functionalities of the API include: +

+ +

+Errormator provides a RESTful API, to make it easy for custom implementations - +it is accessible under following URL: https://api.appenlight.com.

+

All access to the API is secured by https protocol. All data is expected to be sent via json payloads with header Content-Type: application/json

+ +

Official clients

+ +

Currently we have released only python wsgi middleware, + other clients will be released in future. +

+ + + +
+
+la la la lal +
    + + + + + + + + + + + + + + + +
  • + + + + + +
  • + + + + + + + +
  • + + + + +

    + Used to match application only from HTTP_REFERER + - for JS error tracking

    + + +
  • + + + + + + + +
  • + + + + + +
  • + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Reports
# + StatusPr. + When + Error TypeUrl
1500 + 6 + 29 days, 23 hours and 49 minutesHTMLParseError: + malformed start... + www.points2shop.com/withdraws/request_w...
3500 + 6 + 29 days, 22 hours and 40 minutesValueError: invalid + literal for... + www.points2shop.com/offers/free%2Cfreebies
2500 + 6 + 23 days, 9 hours and 33 minutesStaleDataError: + UPDATE statemen... + www.points2shop.com/orders/pay_items
1500 + 6 + 29 days, 23 hours and 57 minutesProgrammingError: + (ProgrammingE... + www.points2shop.com/community/view_frie...
+ + +
+ +{% endblock %} diff --git a/backend/src/appenlight/templates/tests/test.mako b/backend/src/appenlight/templates/tests/test.mako new file mode 100644 index 0000000..7e42482 --- /dev/null +++ b/backend/src/appenlight/templates/tests/test.mako @@ -0,0 +1,5 @@ +<% +import time +time.sleep(0.5) +%> +DUPA diff --git a/backend/src/appenlight/templates/user/lost_password.jinja2 b/backend/src/appenlight/templates/user/lost_password.jinja2 new file mode 100644 index 0000000..1cf1fa5 --- /dev/null +++ b/backend/src/appenlight/templates/user/lost_password.jinja2 @@ -0,0 +1,17 @@ +{% extends "/layout.jinja2" %} +{% block ng_app %}{% endblock %} +{% block content %} + +
+ +
+ {{ widgets.render_flash_messages(flash_msgs) }} +
+ +

{{_('Recover your password')}}

+
+{{widgets.render_form(form)}} +
+
+{% endblock %} diff --git a/backend/src/appenlight/templates/user/lost_password_generate.jinja2 b/backend/src/appenlight/templates/user/lost_password_generate.jinja2 new file mode 100644 index 0000000..8daa21d --- /dev/null +++ b/backend/src/appenlight/templates/user/lost_password_generate.jinja2 @@ -0,0 +1,16 @@ +{% extends "/layout.jinja2" %} +{% block ng_app %}{% endblock %} +{% block content %} +
+ +
+ {{ widgets.render_flash_messages(flash_msgs) }} +
+ +

{{_('Set your new password')}}

+
+{{widgets.render_form(form)}} +
+
+{% endblock %} diff --git a/backend/src/appenlight/templates/user/register.jinja2 b/backend/src/appenlight/templates/user/register.jinja2 new file mode 100644 index 0000000..14a6271 --- /dev/null +++ b/backend/src/appenlight/templates/user/register.jinja2 @@ -0,0 +1,82 @@ +{% extends "/layout.jinja2" %} +{% block ng_app %}{% endblock %} +{% block ng_controller %} data-ng-controller="RegisterController as + register" {% endblock %} +{% block content_class %}two-col equal{% endblock %} +{% block section_name %}register-section{% endblock %} + +{% block page_title %}{% endblock %} + +{% block content %} +
+ +
+ {{ widgets.render_flash_messages(flash_msgs) }} +
+ + + +
+ +
+ + +
+
+

Log in

+
+ {{ widgets.render_form(sign_in_form) }} + + {{ _('Lost password') }} +
+
+ +
+ + +
+
+

Register here

+
+ {{ widgets.render_form(form) }} + +
+
+
+ +
+
+{% endblock %} diff --git a/backend/src/appenlight/templates/widgets.jinja2 b/backend/src/appenlight/templates/widgets.jinja2 new file mode 100644 index 0000000..b34fde1 --- /dev/null +++ b/backend/src/appenlight/templates/widgets.jinja2 @@ -0,0 +1,106 @@ +{% macro print_recursive_old(data) -%} +
    +{% for entry in data|dictsort(true) %} + {% if entry[1].items %} +
  • {{entry[0]}} {{print_recursive(entry[1])}}
  • + {% else %} +
  • {{entry[0]}} {{entry[1]}}
  • + {% endif %} +{% endfor %} +
+{%- endmacro %} + + +{% macro print_recursive(data) -%} + + {% if (data is not mapping and data is not iterable) or data is string %} + {{ data }} + {% else %} + + {% if data is mapping %} + {% for k, v in data.iteritems() %} + + + + + {% endfor %} + {% else %} + {% for item in data %} + + + + + {% endfor %} + {% endif %} +
{{ k }}{{ print_recursive(v) }}
{{ loop.index }}{{ print_recursive(item) }}
+ {% endif %} +{%- endmacro %} + +{% macro render_flash_messages(messages) %} + {% for message in messages%} +
{{message.msg}}
+ {% endfor %} +{% endmacro %} + +{% macro render_paginator(paginator,position='right',first_last= True,_route_name=None) %} +{% if paginator.page_count > 1 %} +
+{{paginator.pager(format='~4~', curpage_attr={'class':'pager_curpage'}, link_attr={'class':'pager_link'})|safe}} +
+{% endif %} +{% endmacro %} + +{% macro render_form(form, mark_required=True, subform=False, horizontal=False) %} +{% for field in form -%} + {% if field.type == 'FormField' -%} +
+ {% for errors in field.errors.values() -%} + {% for error in errors -%} +
{{error}}
+ {% endfor -%} + {% endfor -%} + {{render_form(field, mark_required, subform=True)}} +
+ {% continue -%} + {% endif -%} + {%if field.type not in ['HiddenField','CSRFTokenField'] -%} + {%if not subform -%} + {% if field.errors -%} + {% for error in field.errors -%} +
{{error}}
+ {% endfor -%} + {% endif -%} + {% endif -%} +
+ + {% else %} + + {% endif -%} +{% endfor -%} +{% endmacro %} diff --git a/backend/src/appenlight/tests/__init__.py b/backend/src/appenlight/tests/__init__.py new file mode 100644 index 0000000..f593630 --- /dev/null +++ b/backend/src/appenlight/tests/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + diff --git a/backend/src/appenlight/tests/conftest.py b/backend/src/appenlight/tests/conftest.py new file mode 100644 index 0000000..99b8f90 --- /dev/null +++ b/backend/src/appenlight/tests/conftest.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import mock +import os +import pytest +import transaction +from datetime import datetime + +from alembic.config import Config +from alembic import command +from collections import OrderedDict +from zope.sqlalchemy import mark_changed +from pyramid.paster import get_appsettings +from pyramid import testing + +from appenlight.models import Base, DBSession + + +@pytest.fixture +def base_app(request): + from appenlight import main + import transaction + current_dir = os.path.dirname(os.path.abspath(__file__)) + path = os.path.join(current_dir, '../../../../', + os.environ.get("APPENLIGHT_INI", 'testing.ini')) + # appsettings from ini + app_settings = get_appsettings(path, name="appenlight") + app = main({}, **app_settings) + app_request = testing.DummyRequest(base_url='https://appenlight.com') + app_request.tm = transaction.manager + app_request.add_flash_to_headers = mock.Mock() + testing.setUp(registry=app.registry, request=app_request) + + def teardown(): + testing.tearDown() + + request.addfinalizer(teardown) + + return app + + +@pytest.fixture +def with_migrations(request, base_app): + settings = base_app.registry.settings + alembic_cfg = Config() + alembic_cfg.set_main_option("script_location", + "ziggurat_foundations:migrations") + alembic_cfg.set_main_option("sqlalchemy.url", settings["sqlalchemy.url"]) + command.upgrade(alembic_cfg, "head") + alembic_cfg = Config() + alembic_cfg.set_main_option("script_location", "appenlight:migrations") + alembic_cfg.set_main_option("sqlalchemy.url", settings["sqlalchemy.url"]) + command.upgrade(alembic_cfg, "head") + + for plugin_name, config in base_app.registry.appenlight_plugins.items(): + if config['sqlalchemy_migrations']: + alembic_cfg = Config() + alembic_cfg.set_main_option("script_location", + config['sqlalchemy_migrations']) + alembic_cfg.set_main_option( + "sqlalchemy.url", + base_app.registry.settings["sqlalchemy.url"]) + command.upgrade(alembic_cfg, "head") + + +@pytest.fixture +def default_data(base_app): + from appenlight.models.services.config import ConfigService + from appenlight.lib import get_callable + transaction.begin() + ConfigService.setup_default_values() + for plugin_name, config in base_app.registry.appenlight_plugins.items(): + if config['default_values_setter']: + get_callable(config['default_values_setter'])() + transaction.commit() + + +@pytest.fixture +def clean_tables(): + tables = Base.metadata.tables.keys() + transaction.begin() + for t in tables: + if not t.startswith('alembic_'): + DBSession.execute('truncate %s cascade' % t) + session = DBSession() + mark_changed(session) + transaction.commit() + + +@pytest.fixture +def default_user(): + from appenlight.models.user import User + from appenlight.models.auth_token import AuthToken + transaction.begin() + user = User(user_name='testuser', + status=1, + email='ergo14@gmail.com') + DBSession.add(user) + token = AuthToken(token='1234') + user.auth_tokens.append(token) + DBSession.flush() + transaction.commit() + return user + + +@pytest.fixture +def default_application(default_user): + from appenlight.models.application import Application + + transaction.begin() + application = Application( + resource_id=1, resource_name='testapp', api_key='xxxx') + DBSession.add(application) + default_user.resources.append(application) + DBSession.flush() + transaction.commit() + return application + + +@pytest.fixture +def report_type_matrix(): + from appenlight.models.report import REPORT_TYPE_MATRIX + return REPORT_TYPE_MATRIX + + +@pytest.fixture +def chart_series(): + series = [] + + for x in range(1, 7): + tmp_list = [('key', 'X'), ('0_1', x)] + if x % 2 == 0: + tmp_list.append(('0_2', x)) + if x % 3 == 0: + tmp_list.append(('0_3', x)) + + series.append( + OrderedDict(tmp_list) + ) + return series + + +@pytest.fixture +def log_schema(): + from appenlight.validators import LogListSchema + schema = LogListSchema().bind(utcnow=datetime.utcnow()) + return schema + +@pytest.fixture +def general_metrics_schema(): + from appenlight.validators import GeneralMetricsListSchema + schema = GeneralMetricsListSchema().bind(utcnow=datetime.utcnow()) + return schema + +@pytest.fixture +def request_metrics_schema(): + from appenlight.validators import MetricsListSchema + schema = MetricsListSchema().bind(utcnow=datetime.utcnow()) + return schema + +@pytest.fixture +def report_05_schema(): + from appenlight.validators import ReportListSchema_0_5 + schema = ReportListSchema_0_5().bind(utcnow=datetime.utcnow()) + return schema diff --git a/backend/src/appenlight/tests/payload_examples.py b/backend/src/appenlight/tests/payload_examples.py new file mode 100644 index 0000000..92e15bf --- /dev/null +++ b/backend/src/appenlight/tests/payload_examples.py @@ -0,0 +1,860 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +# -*- coding: utf-8 -*- + +from datetime import datetime + +now = datetime.utcnow().date() + +REQUEST_METRICS_EXAMPLES = [{ + "server": "some.server.hostname", + "timestamp": now.strftime('%Y-%m-%dT%H:%M:%S.0'), + "metrics": [ + ["dir/module:func", + {"custom": 0.0, + "custom_calls": 0, + "main": 0.01664, + "nosql": 0.00061, + "nosql_calls": 23, + "remote": 0.0, + "remote_calls": 0, + "requests": 1, + "sql": 0.00105, + "sql_calls": 2, + "tmpl": 0.0, + "tmpl_calls": 0}], + ["SomeView.function", + {"custom": 0.0, + "custom_calls": 0, + "main": 0.647261, + "nosql": 0.306554, + "nosql_calls": 140, + "remote": 0.0, + "remote_calls": 0, + "requests": 28, + "sql": 0.0, + "sql_calls": 0, + "tmpl": 0.0, + "tmpl_calls": 0}]] +}] + +LOG_EXAMPLES = [ + { + "log_level": "WARNING", + "message": "OMG ValueError happened", + "namespace": "some.namespace.indicator", + "request_id": "SOME_UUID", + "server": "some server", + "tags": [["tag_name", "tag_value"], + ["tag_name2", 2] + + ], + "date": now.strftime('%Y-%m-%dT%H:%M:%S.%f') + }, + { + "log_level": "ERROR", + "message": "OMG ValueError happened2", + "namespace": "some.namespace.indicator", + "request_id": "SOME_UUID", + "server": "some server", + "date": now.strftime('%Y-%m-%dT%H:%M:%S.%f') + } +] + +PARSED_REPORT_404 = { + 'report_details': [{ + 'username': 'foo', + 'url': 'http://localhost:6543/test/error?aaa=1&bbb=2', + 'ip': '127.0.0.1', + 'start_time': now.strftime( + '%Y-%m-%dT%H:%M:%S.0'), + 'slow_calls': [], + 'request': {'COOKIES': { + 'country': 'US', + 'sessionId': '***', + 'test_group_id': '5', + 'http_referer': 'http://localhost:5000/'}, + 'POST': {}, + 'GET': {'aaa': ['1'], 'bbb': ['2']}, + 'HTTP_METHOD': 'GET', + }, + 'user_agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0.1) Gecko/20100101 Firefox/10.0.1', + 'message': '', + 'end_time': now.strftime( + '%Y-%m-%dT%H:%M:%S.0'), + 'request_stats': {} + }], + 'error': '404 Not Found', + 'server': "servername/instancename", + 'priority': 5, + 'client': 'appenlight-python', + 'language': 'python', + 'http_status': 404} + +PYTHON_PAYLOAD_0_4 = { + "client": "your-client-name-python", + "language": "python", + 'view_name': 'views/foo:bar', + 'server': "servername/instancename", + "priority": 5, + "error": "OMG ValueError happened test", + "occurences": 1, + "http_status": 500, + "report_details": [ + {"username": "USER", + "url": "HTTP://SOMEURL", + "ip": "127.0.0.1", + "start_time": now.strftime( + '%Y-%m-%dT%H:%M:%S.0'), + "end_time": now.strftime( + '%Y-%m-%dT%H:%M:%S.0'), + "user_agent": "BROWSER_AGENT", + "message": "arbitrary text that will get attached to your report", + "request_id": "SOME_UUID", + "request": {"REQUEST_METHOD": "GET", + "PATH_INFO": "/FOO/BAR", + "POST": {"FOO": "BAZ", "XXX": "YYY"} + }, + "slow_calls": [ + { + "type": "sql", + "start": now.strftime( + '%Y-%m-%dT%H:%M:%S.0'), + "end": now.strftime( + '%Y-%m-%dT%H:%M:%S.0'), + "subtype": "postgresql", + "parameters": ["QPARAM1", "QPARAM2", + "QPARAMX"], + "statement": "QUERY"}], + "request_stats": {"main": 0.50779, + "nosql": 0.01008, + "nosql_calls": 17.0, + "remote": 0.0, + "remote_calls": 0.0, + "custom": 0.0, + "custom_calls": 0.0, + "sql": 0.42423, + "sql_calls": 1.0, + "tmpl": 0.0, + "tmpl_calls": 0.0}, + "traceback": [ + {"cline": "return foo_bar_baz(1,2,3)", + "file": "somedir/somefile.py", + "fn": "somefunction", + "line": 454, + "vars": [["a_list", + ["1", + "2", + "4", + "5", + "6"]], + ["b", + {1: "2", "ccc": "ddd", "1": "a"}], + ["obj", + ""]]}, + {"cline": "OMG ValueError happened", + "file": "", + "fn": "", + "line": "", + "vars": []}] + }] +} + +PYTHON_PAYLOAD_0_5 = { + "client": "your-client-name-python", + "language": "python", + 'view_name': 'views/foo:bar', + 'server': "servername/instancename", + "priority": 5, + "error": "OMG ValueError happened test", + "occurences": 1, + "http_status": 500, + "username": "USER", + "url": "HTTP://SOMEURL", + "ip": "127.0.0.1", + "start_time": now.strftime( + '%Y-%m-%dT%H:%M:%S.0'), + "end_time": now.strftime( + '%Y-%m-%dT%H:%M:%S.0'), + "user_agent": "BROWSER_AGENT", + "message": "arbitrary text that will get attached to your report", + "request_id": "SOME_UUID", + "request": {"REQUEST_METHOD": "GET", + "PATH_INFO": "/FOO/BAR", + "POST": {"FOO": "BAZ", "XXX": "YYY"}}, + "slow_calls": [ + { + "type": "sql", + "start": now.strftime( + '%Y-%m-%dT%H:%M:%S.0'), + "end": now.strftime( + '%Y-%m-%dT%H:%M:%S.0'), + "subtype": "postgresql", + "parameters": ["QPARAM1", "QPARAM2", "QPARAMX"], + "statement": "QUERY"}], + "request_stats": {"main": 0.50779, + "nosql": 0.01008, + "nosql_calls": 17.0, + "remote": 0.0, + "remote_calls": 0.0, + "custom": 0.0, + "custom_calls": 0.0, + "sql": 0.42423, + "sql_calls": 1.0, + "tmpl": 0.0, + "tmpl_calls": 0.0}, + "traceback": [ + {"cline": "return foo_bar_baz(1,2,3)", + "file": "somedir/somefile.py", + "fn": "somefunction", + "line": 454, + "vars": [["a_list", + ["1", + "2", + "4", + "5", + "6"]], + ["b", {1: "2", "ccc": "ddd", "1": "a"}], + ["obj", + ""]]}, + {"cline": "OMG ValueError happened", + "file": "", + "fn": "", + "line": "", + "vars": []}] +} + +PHP_PAYLOAD = { + 'client': 'php', + 'error': 'Nie mo\u017cna ustali\u0107 \u017c\u0105dania "feed.xml".', + 'error_type': '', + 'http_status': 404, + 'language': 'unknown', + 'priority': 1, + 'report_details': [{'end_time': None, + 'group_string': None, + 'ip': None, + 'message': 'exception \'CHttpException\' with message \'Nie mo\u017cna ustali\u0107 \u017c\u0105dania "feed.xml".\' in /home/dobryslownik/www/sites/dobryslownik/vendor/yiisoft/yii/framework/web/CWebApplication.php:286\nStack trace:\n#0 /home/dobryslownik/www/sites/dobryslownik/common/components/WebApplication.php(34): CWebApplication->runController(\'feed.xml\')\n#1 /home/dobryslownik/www/sites/dobryslownik/vendor/yiisoft/yii/framework/web/CWebApplication.php(141): WebApplication->runController(\'feed.xml\')\n#2 /home/dobryslownik/www/sites/dobryslownik/vendor/yiisoft/yii/framework/base/CApplication.php(180): CWebApplication->processRequest()\n#3 /home/dobryslownik/www/sites/dobryslownik/frontend/www/index.php(23): CApplication->run()\n#4 {main}', + 'occurences': 1, + 'request': { + 'COOKIES': [], + 'FILES': [], + 'GET': [], + 'POST': [], + 'REQUEST_METHOD': None, + 'SERVER': { + 'DOCUMENT_ROOT': '/home/dobryslownik/www/sites/dobryslownik/frontend/www/', + 'GATEWAY_INTERFACE': 'CGI/1.1', + 'HTTPS': 'on', + 'HTTP_ACCEPT': '*/*', + 'HTTP_ACCEPT_ENCODING': 'gzip, deflate', + 'HTTP_ACCEPT_LANGUAGE': 'pl-PL', + 'HTTP_CONNECTION': 'close', + 'HTTP_HOST': 'dobryslownik.pl', + 'HTTP_IF_MODIFIED_SINCE': 'Wed, 30 Jul 2014 18:26:32 GMT', + 'HTTP_IF_NONE_MATCH': '"45de3-2a3-4ff6d4b9fbe7f"', + 'HTTP_USER_AGENT': 'Apple-PubSub/28', + 'HTTP_X_FORWARDED_FOR': '195.150.190.186', + 'HTTP_X_FORWARDED_PROTO': 'https', + 'PATH': '/bin:/usr/bin:/usr/ucb:/usr/bsd:/usr/local/bin', + 'PHP_SELF': '/index.php', + 'QUERY_STRING': '', + 'REDIRECT_HTTPS': 'on', + 'REDIRECT_STATUS': '200', + 'REDIRECT_UNIQUE_ID': 'VFAhZQoCaXIAAAkd414AAAAC', + 'REDIRECT_URL': '/feed.xml', + 'REMOTE_ADDR': '195.150.190.186', + 'REMOTE_PORT': '41728', + 'REQUEST_METHOD': 'GET', + 'REQUEST_TIME': 1414537573, + 'REQUEST_TIME_FLOAT': 1414537573.32, + 'REQUEST_URI': '/feed.xml', + 'SCRIPT_FILENAME': '/home/dobryslownik/www/sites/dobryslownik/frontend/www/index.php', + 'SCRIPT_NAME': '/index.php', + 'SERVER_ADDR': '10.2.105.114', + 'SERVER_ADMIN': '[no address given]', + 'SERVER_NAME': 'dobryslownik.pl', + 'SERVER_SIGNATURE': '', + 'SERVER_SOFTWARE': 'Apache/2.2.22 (Ubuntu) PHP/5.4.17', + 'UNIQUE_ID': 'VFAg4AoCaXIAAAkd40UAAAAC'}, + 'SESSION': []}, + 'request_id': 'VFAg4AoCaXIAAAkd40UAAAAC', + 'request_stats': {'custom': 0, + 'custom_calls': 0, + 'main': 0, + 'nosql': 0.0, + 'nosql_calls': 0.0, + 'remote': 0.0, + 'remote_calls': 0.0, + 'sql': 0.0, + 'sql_calls': 0.0, + 'tmpl': 0.0, + 'tmpl_calls': 0.0, + 'unknown': 0.0}, + 'slow_calls': [], + 'start_time': None, + 'frameinfo': [ + {'cline': None, + 'file': '/home/dobryslownik/www/sites/dobryslownik/common/components/WebApplication.php', + 'fn': 'CWebApplication->runController', + 'line': 34, + 'vars': ['feed.xml']}, + {'cline': None, + 'file': '/home/dobryslownik/www/sites/dobryslownik/vendor/yiisoft/yii/framework/web/CWebApplication.php', + 'fn': 'WebApplication->runController', + 'line': 141, + 'vars': ['feed.xml']}, + {'cline': None, + 'file': '/home/dobryslownik/www/sites/dobryslownik/vendor/yiisoft/yii/framework/base/CApplication.php', + 'fn': 'CWebApplication->processRequest', + 'line': 180, + 'vars': []}, + {'cline': None, + 'file': '/home/dobryslownik/www/sites/dobryslownik/frontend/www/index.php', + 'fn': 'CApplication->run', + 'line': 23, + 'vars': []}], + 'url': 'https://dobryslownik.pl/feed.xml', + 'user_agent': 'magpie-crawler/1.1 (U; Linux amd64; en-GB; +http://www.brandwatch.net)', + 'username': 'guest'}], + 'server': 'unknown', + 'traceback': '', + 'view_name': ''} + +JS_PAYLOAD = { + "client": "javascript", "language": "javascript", + "error_type": "ReferenceError: non_existant_var is not defined", + "occurences": 1, "priority": 5, "server": "jstest.appenlight", + "http_status": 500, "report_details": [{ + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36", + "start_time": "2014-10-29T19:59:30.589Z", + "frameinfo": [ + { + "cline": "", + "file": "http://demo.appenlight.com/#", + "fn": "HTMLAnchorElement.onclick", + "line": 79, + "vars": []}, + { + "cline": "", + "file": "http://demo.appenlight.com/static/js/demo.js", + "fn": "test_error", + "line": 7, + "vars": []}, + { + "cline": "ReferenceError: non_existant_var is not defined", + "file": "http://demo.appenlight.com/static/js/demo.js", + "fn": "something", + "line": 2, + "vars": []}], + "url": "http://demo.appenlight.com/#", + "server": "jstest.appenlight", + "username": "i_am_mario", + "ip": "127.0.0.1", + "request_id": "0.01984176435507834"}]} + +AIRBRAKE_RUBY_EXAMPLE = """ + + + APPENLIGHT_API_KEY + + Airbrake Notifier + 3.1.7 + https://github.com/airbrake/airbrake + + + NameError + NameError: undefined local variable or method `sdfdfdf' for #<#<Class:0x000000039a8b90>:0x00000002c53df0> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + http://0.0.0.0:3000/welcome/index?test=1234 + welcome + index + + 1234 + welcome + index + + + 4706af9678e4b94f2bb66e1d85ced382 + h9R7MuRtnNX6ZDK6vI1pIJV3dYYtlEx1mPw/nzyIVTA= + + + CGI/1.1 + /welcome/index + test=1234 + 127.0.0.1 + localhost + GET + http://0.0.0.0:3000/welcome/index?test=1234 + + 0.0.0.0 + 3000 + HTTP/1.1 + WEBrick/1.3.1 (Ruby/1.9.3/2012-11-10) + 0.0.0.0:3000 + keep-alive + max-age=0 + text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.56 Safari/537.17 + 1 + gzip,deflate,sdch + pl,en-US;q=0.8,en;q=0.6 + ISO-8859-1,utf-8;q=0.7,*;q=0.3 + _rails_app_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTQ3MDZhZjk2NzhlNGI5NGYyYmI2NmUxZDg1Y2VkMzgyBjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMWg5UjdNdVJ0bk5YNlpESzZ2STFwSUpWM2RZWXRsRXgxbVB3L256eUlWVEE9BjsARg%3D%3D--08a5940133e1c7f7ca58ed5154e3a8e7acae337a + ["1", "1"] + #<StringIO:0x00000003b6ca08> + #<IO:0x00000001e76228> + false + false + false + http + HTTP/1.1 + /welcome/index + /welcome/index?test=1234 + #<ActionDispatch::Routing::RouteSet:0x00000002b31648> + ["password"] + true + true + #<ActiveSupport::TaggedLogging:0x00000002ae79d0> + #<Rails::BacktraceCleaner:0x000000029a7250> + c11b2267f3ad8b00a1768cae35559fa1 + 127.0.0.1 + + 4706af9678e4b94f2bb66e1d85ced382 + h9R7MuRtnNX6ZDK6vI1pIJV3dYYtlEx1mPw/nzyIVTA= + + + / + + + false + true + false + false + e96386a74a0c95f5c8eeaf95e47d14962973a0ff9e9d32841703559423b1 + #<Rack::Session::Cookie::Base64::Marshal:0x00000002daf528> + 4706af9678e4b94f2bb66e1d85ced382 + + + BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTQ3MDZhZjk2NzhlNGI5NGYyYmI2NmUxZDg1Y2VkMzgyBjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMWg5UjdNdVJ0bk5YNlpESzZ2STFwSUpWM2RZWXRsRXgxbVB3L256eUlWVEE9BjsARg==--08a5940133e1c7f7ca58ed5154e3a8e7acae337a + + _rails_app_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTQ3MDZhZjk2NzhlNGI5NGYyYmI2NmUxZDg1Y2VkMzgyBjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMWg5UjdNdVJ0bk5YNlpESzZ2STFwSUpWM2RZWXRsRXgxbVB3L256eUlWVEE9BjsARg%3D%3D--08a5940133e1c7f7ca58ed5154e3a8e7acae337a + #<ActionDispatch::Cookies::CookieJar:0x00000002c51e38> + + 4706af9678e4b94f2bb66e1d85ced382 + h9R7MuRtnNX6ZDK6vI1pIJV3dYYtlEx1mPw/nzyIVTA= + + + welcome + index + + #<WelcomeController:0x00000002c4f020> + + + test=1234 + + 1234 + + + 1234 + + + 1234 + welcome + index + + ["text/html"] + + + + /home/ergo/workspace/rails_app + development + ergo-desktop + + Rails: 3.2.11 + +""".replace('\n', '').replace(' ', '') + +AIRBRAKE_EXAMPLE_SHORT = ''' + + + 76fdb93ab2cf276ec080671a8b3d3866 + + Airbrake Notifier + 3.1.6 + http://api.airbrake.io + + + RuntimeError + RuntimeError: I've made a huge mistake + + + + + + + http://example.com + + + + example.org + Mozilla + + + + /testapp + production + 1.0.0 + + +'''.replace('\n', '').replace(' ', '') + +SENTRY_PYTHON_PAYLOAD_7 = { + 'culprit': 'djangoapp.views in error', + 'event_id': '9fae652c8c1c4d6a8eee09260f613a98', + 'exception': { + 'values': [ + {'module': 'exceptions', + 'stacktrace': {'frames': [{ + 'abs_path': '/home/ergo/venvs/appenlight/local/lib/python2.7/site-packages/django/core/handlers/base.py', + 'context_line': 'response = wrapped_callback(request, *callback_args, **callback_kwargs)', + 'filename': 'django/core/handlers/base.py', + 'function': 'get_response', + 'in_app': False, + 'lineno': 111, + 'module': 'django.core.handlers.base', + 'post_context': [ + ' except Exception as e:', + ' # If the view raised an exception, run it through exception', + ' # middleware, and if the exception middleware returns a', + ' # response, use that. Otherwise, reraise the exception.', + ' for middleware_method in self._exception_middleware:'], + 'pre_context': [ + ' break', + '', + ' if response is None:', + ' wrapped_callback = self.make_view_atomic(callback)', + ' try:'], + 'vars': { + 'callback': '', + 'callback_args': [], + 'callback_kwargs': {}, + 'e': "Exception(u'test 500 \\u0142\\xf3\\u201c\\u0107\\u201c\\u0107\\u017c\\u0105',)", + 'middleware_method': '>', + 'request': '', + 'resolver': "", + 'resolver_match': "ResolverMatch(func=, args=(), kwargs={}, url_name='error', app_name='None', namespace='')", + 'response': None, + 'self': '', + 'urlconf': "'djangoapp.urls'", + 'wrapped_callback': ''}}, + { + 'abs_path': '/home/ergo/IdeaProjects/django_raven/djangoapp/views.py', + 'context_line': "raise Exception(u'test 500 \u0142\xf3\u201c\u0107\u201c\u0107\u017c\u0105')", + 'filename': 'djangoapp/views.py', + 'function': 'error', + 'in_app': False, + 'lineno': 84, + 'module': 'djangoapp.views', + 'post_context': [ + '', + '', + 'def notfound(request):', + " raise Http404('404 appenlight exception test')", + ''], + 'pre_context': [ + ' c.execute("INSERT INTO stocks VALUES (\'2006-01-05\',\'BUY\',\'RHAT\',100,35.14)")', + ' c.execute("INSERT INTO stocks VALUES (\'2006-01-05\',\'BUY\',\'RHAT\',100,35.14)")', + ' conn.commit()', + ' c.close()', + " request.POST.get('DUPA')"], + 'vars': { + 'c': '', + 'conn': '', + 'request': ''}}]}, + 'type': 'Exception', + 'value': 'test 500 \u0142\xf3\u201c\u0107\u201c\u0107\u017c\u0105'}]}, + 'extra': { + 'sys.argv': ["'manage.py'", "'runserver'"]}, + 'level': 40, + 'message': 'Exception: test 500 \u0142\xf3\u201c\u0107\u201c\u0107\u017c\u0105', + 'modules': {'django': '1.7.1', + 'python': '2.7.6', + 'raven': '5.9.2'}, + 'platform': 'python', + 'project': 'sentry', + 'release': 'test', + 'request': {'cookies': { + 'appenlight': 'X'}, + 'data': None, + 'env': {'REMOTE_ADDR': '127.0.0.1', + 'SERVER_NAME': 'localhost', + 'SERVER_PORT': '8000'}, + 'headers': { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Encoding': 'gzip, deflate, sdch', + 'Accept-Language': 'en-US,en;q=0.8,pl;q=0.6', + 'Connection': 'keep-alive', + 'Content-Length': '', + 'Content-Type': 'text/plain', + 'Cookie': 'appenlight=X', + 'Dnt': '1', + 'Host': '127.0.0.1:8000', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36'}, + 'method': 'GET', + 'query_string': '', + 'url': 'http://127.0.0.1:8000/error'}, + 'server_name': 'ergo-virtual-machine', + 'tags': {'site': 'example.com'}, + 'time_spent': None, + 'timestamp': now.strftime('%Y-%m-%dT%H:%M:%SZ')} + + +SENTRY_JS_PAYLOAD_7 = { + "project": "sentry", "logger": "javascript", + "platform": "javascript", "request": {"headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36"}, + "url": "http://localhost:6543/test/js_error#/"}, + "exception": {"values": [{"type": "ReferenceError", + "value": "fateqtwetew is not defined", + "stacktrace": {"frames": [{ + "filename": "https://cdn.ravenjs.com/2.0.0/angular/raven.min.js", + "lineno": 1, + "colno": 4466, + "function": "c", + "in_app": False}, + { + "filename": "http://localhost:6543/test/js_error", + "lineno": 47, + "colno": 19, + "function": "?", + "in_app": True}]}}]}, + "culprit": "http://localhost:6543/test/js_error", + "message": "ReferenceError: fateqtwetew is not defined", + "extra": {"session:duration": 5009}, + "event_id": "2bf514aaf0e94f35a8f435a0d29a888b"} + +SENTRY_JS_PAYLOAD_7_2 = { + "project": "sentry", "logger": "javascript", + "platform": "javascript", "request": {"headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36"}, + "url": "http://localhost:6543/#/report/927/9558"}, + "exception": {"values": [{"type": "Error", + "value": "[$injector:modulerr] http://errors.angularjs.org/1.5.0-rc.0/$injector/modulerr?p0=appenlight&p1=Erro…", + "stacktrace": {"frames": [{ + "filename": "http://localhost:6543/static/js/appenlight.js?rev=752", + "lineno": 1647, + "colno": 112, + "function": "?", + "in_app": True}, + { + "filename": "http://localhost:6543/static/js/appenlight.js?rev=752", + "lineno": 1363, + "colno": 41, + "function": "be", + "in_app": True}, + { + "filename": "http://localhost:6543/static/js/appenlight.js?rev=752", + "lineno": 1364, + "colno": 225, + "function": "zc", + "in_app": True}, + { + "filename": "http://localhost:6543/static/js/appenlight.js?rev=752", + "lineno": 1363, + "colno": 421, + "function": "c", + "in_app": True}, + { + "filename": "http://localhost:6543/static/js/appenlight.js?rev=752", + "lineno": 1386, + "colno": 360, + "function": "fb", + "in_app": True}, + { + "filename": "http://localhost:6543/static/js/appenlight.js?rev=752", + "lineno": 1383, + "colno": 49, + "function": "g", + "in_app": True}, + { + "filename": "http://localhost:6543/static/js/appenlight.js?rev=752", + "lineno": 1351, + "colno": 344, + "function": "n", + "in_app": True}, + { + "filename": "http://localhost:6543/static/js/appenlight.js?rev=752", + "lineno": 1383, + "colno": 475, + "function": "?", + "in_app": True}, + { + "filename": "http://localhost:6543/static/js/appenlight.js?rev=752", + "lineno": 1350, + "colno": 421, + "function": "?", + "in_app": True}]}}]}, + "culprit": "http://localhost:6543/static/js/appenlight.js?rev=752", + "message": "Error: [$injector:modulerr] http://errors.angularjs.org/1.5.0-rc.0/$injector/modulerr?p0=appenlight&…", + "extra": {"session:duration": 330}, + "event_id": "c50b5b6a13994f54b1d8da0c2e0e767a"} + +SENTRY_LOG_PAYLOAD_7 = { + "project": "sentry", "sentry.interfaces.Message": { + "message": "TEST from django logging", "params": []}, + "server_name": "ergo-virtual-machine", + "culprit": "testlogger in index", + "extra": {"thread": 139723601139456, "process": 24645, + "sys.argv": ["'manage.py'", "'runserver'"], + "price": 6, "threadName": "'Thread-1'", + "filename": "'views.py'", + "processName": "'MainProcess'", + "tag": "'extra'", "dupa": True, "lineno": 22, + "asctime": "'2016-01-18 05:24:29,001'", + "pathname": "'/home/ergo/IdeaProjects/django_raven/djangoapp/views.py'"}, + "event_id": "9a6172f2e6d2444582f83a6c333d9cfb", + "timestamp": now.strftime('%Y-%m-%dT%H:%M:%SZ'), + "tags": {"site": "example.com"}, + "modules": {"python": "2.7.6", "raven": "5.9.2", + "django": "1.7.1"}, "time_spent": None, + "platform": "python", "release": "test", + "logger": "testlogger", "level": 50, + "message": "TEST from django logging"} + +METRICS_PAYLOAD = { + "namespace": "some.monitor", + "timestamp": now.strftime('%Y-%m-%dT%H:%M:%S.0'), + "server_name": "server.name", + "tags": [["usage_foo", 15.5], + ["usage_bar", 63], + ] +} + + +SENTRY_PYTHON_ENCODED = b'x\x9c\xedXmo\xdbF\x12\xfe+\x0b\xdd\x07I9\x89/z\x97[\x05p\x1d5\x0ej\'\x81,\xa7\xed\xd59bE\x8d$V|\xcb\xeeR\x96k\xf8\xbf\xdf\xcc.II\xb6\xdc\xf4\xd2\x16\xb8\x03\xa2 \x06\xb9\xdc\x9d\x9dy\xe6\x99y\x96\xbc\xaf\xa4"\xf9\x15|U9a\x15\t\xb1\x12w\x95\x06\xab\xc0\xd6\x87T\x05I\x8c\xc3\xf7\x95\r\x0f3\x90x\xf9\xcb}E*\xee\xaf\x95\xe0>\xe8G\x0b\xc1\xa3\xe2\xd1"\x8b\xfd|Me\t\xca\x13 \xd3$\x96@\x06\xf9Lz)W+zf\xaf\x92\x08l\x10\xcb\xc4\xde@\xbc\x91vz\xa7VI\xdc\xb2\xc3\xc4\xe7\xa1\x1d\x06\xb3b\xc4\xea\xdb2P\xd0LqO\xbe\x04i\xcf\x7f\xe51.\xf3\x13\x01\xf6\x8a\xc7\xf3\x10\x84\xb4g\\\x82\x95j\xbfS\x01\x9e\x9f\xc4\n\xb6\x14\xd0/\x15\xb6\xf7\x0b\x16\xac\xf0\x88\x05\x92\xbdMb8\xa15\xec\xd1\xefV\xf04\x85\xb9\x87\xbe\x843\xdc\x98\x8d\x98\x84paE|\r\xde&\x80[\x8f\xab$\n\xfcZ1\xa1~\xcc\n\x02y\xd4:\xfdJ7FO6\xab\t\xf8\x84X\xab\x06{Q\x0cy\\,%\xde\xef\x06\xd6\xb74tt[\x9386.\xf2\xc7\xb8d\x18\xe6G\xc2&\x91\xea\x00\x9c\xc7\xeb\xff\xc1\xce\x92(\ry\x10\x13Vj\x05\x8c\xa2EoU&b\x98k\xc4X\x8d3?\x89"\xb4\x0cB$\xa2n=\xb6\xf2Ga\xc6y\x81\x0cb\xe4S\xecC\x89e\x83\xa9\xbb\x14\xa4\xf5}\xce\xa5)\xde\xd5O\x8cw\xdf\x7f\xf7\xe19DuZb\xa4"BZ\x98\xb2<=\xe2y:\xfa\r\x17R3\x96x[)\xf1\xa9eU\x85p\xb3\xae\xe3\xb0\x9b\x9b\xccq;\xad\x9b\x9b\xed\xa2\x8d\xd7-\xc7\xf5\xf5\x90\xd3\x7fr\xe7\xb8\xfd\xfc\xae[m\xe8D\x1cd\x8b\xe0\xa5M\x11\x88$\xdc\x80\xf0"\xae|\xcd\xfdI>rI\x035\xaa\x98\x91\xe14\xd2\xc0\xa2(\xa4\xa5qm0\xb23\xaa\xd5\x1b\xccd{t\xff\xd0`\x99\x08uL\xa3bN\x9a\xea{9\xa2\xed\xf4\x15\x96\x8a\xbe\xd5N\x15\x89\xf0\x02\x89\xd5\x18\xcfA\xc0\x1c\xbdX\xf0P\x02>\x8e\x829V\x10\x9a\x07/\x02,8zV\xf9v\x96d\xf1\x9c\x99\x01v\tRb\xe5]\x963-\xec\x17\xb8\x03\xd9\xd3De\xc9\x82}kB\xb0\x88\\"\x98Y\x91Y$\xad\xdd\x06\xd6\x13C,\x99Q\xdfa\\1g\xdb_\xb4{\xdd\xb6\x03}\x7f\xe8\xbc|I\xaeS\xc9iwJ\xdbh\xa4(y\xebV.\x03\xeb\xc7\xab\xd7o\xce\xcd\xc8ScC\xd7\x1f\xf6\xba\xceK\x03\x83vU\x9b\xa3E\x93\xdcu=\xdbm\x0f\x07}\xb75p;\xbdA\xabc\x16\x14\xc9\xd4+\x8a\xb6f\x08\xcf\x16"\x89\xd8\xa3\x9c\xed\x07\xe1\xf7\x06\x83\xce@\x9by\\\xdc\x7f\xd2\\\xc1&mf\x02K\xd8^O.\nB\xb1\xea\xce\x08\xd2DVYM\x97\x1e\xfd\xa9\xb3\x7f\xdb\x07q\xe5\x1d\x84\xea\xe1a\x8f&x\x1fga\x88#h\x01\x93\xa9\x13\xf0\xd8n\x85VD\xc9<\x0bu%\x1dM\x0fud\xdao\x11\x84@\xac\xdcM|\xbeu\x87A\x0cq\x823\xdd\xce\x10o\x83\xd8\xc3-\xf7\xc8\x9aw.\x8f\xe6\x91\xbd\xcf4V\xdd\xb2\x0b\xae\x96r\xe6\xcd\xee\xbc\x1d)kh7\xe7F\x9d\xc2\xfa\x9f\x97\xb0\xfd\xdfL\x00_\xd3\x82/m\xc0\x7f\xa1\xce\x1d\x95\x97\xc73\x9f\x91\xa6\xcfk\xe4\x7f\x9d\xca#\xa0\xfc\x8d\xda\xf6U]\xbe\xaa\xcbWu\xf9\xffQ\x97\xfe_\xa0.\x7f\xea\xd8\xfeDit\xae~Gbn\x13\xb1\xd6\xa5\x97\x8b\x87\'8\xaa\x8e]Bg\x9b\xd2~^?|\x0b\xb6\xe0g\nj7\x957o\xaf\xc6\x93){\xf3v\xfa\x8eI\x95\xf8k\xc9>\x9c^\\\x8f\xafX\xad\xdar\x9c^\xd3q\x9b\xd4w\xaa\xdf]\xff\x8c\x7f\'\xe7\xa7\xd3j\xc3u\x9cF\xbbk\xb9\x9d\xfaM\xa5\x94\x81\xbf\xc9j\x12\xc7\x16\xb5\xe1@\xd5\xf6\xb6\xf2\xc3D\xc2n \xc7\xdbz\xff\xeejj\xa1R\xd7\xaa\xaf\xae\xdf\x9fV\xeb\xcf\xbf\xe9\xd0\xff9,X\x9c\xa8\x05\xf5\xa0"e\xf5R\x82\x04\x0f0\xb9\xe7J\xa5\x1d\xa7S\xab\xe2\x1fSE\xd8^\xb1\x94J\xe1a\xd4\xd2\xabFe\x0ez\xbf\xafKG~\nQ\xef\xdb\xd6Y&dr\xa4u\xb4\x1c\xde\x99\x7fq\xeb@p\x0ew\xc1\x010\x15\x7f\xa4\xe3\xcd\x17\xfd>.\xcc\x16\xb3N\xdb\x1f\xce\x16=\x07\xda\xb3\xd6\x00|\x7f\x01\xd0=\xe4\x1623I\xd6\x01\x18\x96R\x9f\xcf\x84\xfe|Sq55\xb0\xf1\xd2\xcd\n\x89\x7fb\xdbn\xabo9\xf8\xcf=\x198\x8ec\x97\xd1\xad\x80\xa3\xc2\x1b\x1bg\x94\xeeX5/ ^\x9anE3N}B\xbfy\x81\x08e\x18\x89\xc6 n^_5 \xfe\xe6\xd3\xc8\xb1\x06\x8d4\xd4\x17\xbd\xbd\xd9\xe3\xd8O\xe6A\xbc\xd4\'\xee\xdf\x82\xb4\xc1\xb0HC\xae\x90Ur\x8e\xa7\x1a\x9c\xb9\xe38MZ\x03\xa4M\x1e\x06\x1b\xd8Y1I\xde*{\xa5\xa2\xb0\x81\xd9\t\x03\x9f\xd3\x02{K#\xff\xdc>\x1e\x8d\x8c#\xc3F\x10\xa1\xa7\xf6-\xcc\xd2\xc6\x0b\xfb\x85q\x93\xec^\xa7K\x81\xf16\xdf`\x12\xfcL@3/Mi`\xc3\x19\xafbU^\x9f\'\xa6\x88\x0f\xb13\xbe\x13\xf2\xf4\xac\xc0}\xa4W\x9c!\x1f\xa0I8\x8aD\xa3\x1f\xf1m\x13]\x19\xe9U\xd7\x98\xf9\xe6\xe9\x12\xcc\x16\x97\xc9oA\x18r\xbbk9\xac\xf6\x93\xeb~\xc3.\x828\xdb\xb2\xed\xa0\xe7\xf5:uv\x8a\x91\xc1\x8f0\xfb!Pv\xb7\xdd\xb7\xda=V\xfb\xe1|zy\xd1`a\xb0\x06\xf6\x1a\xfcuRgg+A\x8a\xd2%\x07[\xbd\x9ek\r;\xec\x8a/\xb8\x08\xf2U9\xd6:\xb3\xd3\xbc\xd04\xaa\xfa\xdc\xac\xa9\x82\xef:\x9a\x00\xd8\xec?\x8c\'\xde\xdb\xd3\xcb1\xcd\xd2o=+\x02\x01\xe7\xe4\xcf\xde\xbf\x9bL\xe9Y\x81\xc4d|\xf9n:\xf6N_\xbd\x9a\x1c@\xa5\xed"\xb6\xe2\xce\x93x\xbe3L\xd0\xcd\x9a+\xbe;3\xec\x8e\x90\xaf\xc7S\xbd&\xc4\x8a \xe8:N\xd9\x03\x0c;\xcd\x9b\x17M\xc5\xb7/C6-\x984Bjci\x7fL%kW\xac!\xce\xd2\xed%\x88\xc0\x93\xa9\xc1=\xdf\x18\x83G\xc1\x10\x11\xcd\xcc-\xe73\xa5\xe2Q\xaa\xb7q\\\x14\xb8n\xd3\xe9L\xdd\xdeI\xc79i\r\xffE\x93\xe8\x15m\xee\x8b,\x9a\xc9\'\xdfQ\x91\x89\xb0L\xc4]\xd1\x9f\xc2d\xb9\x04]hE\\\xbbc\xc1\xfef\xa8\x06\xad6b\xda\x1aZ\x9dn\xbf7\xdc\x01u\xbf\xd7Y\xb0]\xe9\n\xef\xd1j\xbe4\xbd\x91\x1e6\n\xb3\'\xf8\xe6\x96\xc1\x03E=\xcf\x04\xcf\xab\xab\x04[\x1f\xa7i\xd9t|5\xdd?F2r\x94\xb2\xb4\xd7\x8d\xb1by\x16*s\xb0\xd9\x0f\xcc\xc0\xbe\x1f\xd3\x1cf\xd9\xf2wc\x1at\x9d\xfe^P\x9fu\xf0\n\xdf<\xd0\x1f\x96\x0f\xd1\x1bC\xa8\xdb\x12\xeb\xf6\xfbL%\xecH_\x1b86O\x03\xdb|\xef\xb6\xf1\xbc\x82\xa7\xc6\xa3\x01}4\x07\xd8\x10\xb8,\x95\xa4r\xb8\x7f)E\'\xec\xcbu\xc6\xa4\xc9\xb0\x84>\x17\x98\x84!:!\xd0YH\x93S\xce\xd7\x86E\xd8\x85\xf3^\xb8cs!:\x1a\xf1f\xce\xd3\x87\x87\xff\x00`\xb1k\xbd' + +SENTRY_RUBY_ENCODED = b'eJzVVttu4zYQ/RVCfUgLRNfYsi002z5sf6DoU7cLgyJHEmOKFEjKW6+Rf++QkmM7ziJF0T7UMBzNhZzLOTPKMYI9KLcVPKoioDndLAvIH5YlrykHVpd8sSlhXfNstWTRfdSDtbQFdP4djP4o9sIKrX4xRpuKcBQ5cFIfSIa+TqC3o/2A3kWWl3G2jLPFb3lZLcuqyGePrR0wgahSo5T3kcR0ZFQtsvtoMPoJ2ItlkNQ12vR4mRnrA56Wum3BoIzPbJSDEegcOYyZmJoIRVJCHZFCAdmgiwWzB7NVtPfpg2l1vBfGjVTGPWUduvn6NB8l2Kg6RpQ5LK2nQoYgi6RISvSY1ANluxvlXsCXV8o9POn6RodRfJWvtAaYNvxGbcdh0MZd6k04XSZZ8oBiLVqESvTUK/PZpx6F5CHxB9QUQaP4VEqe5EWSn1XxqKSmPFiy4Mu0YqMxCEwcmn22AMrCekSTVeJRhj+BjY7WEuJO650Nvg/Bt6GGcupPZ8kmaFro4y+GDgMYOye78mqpayoDB7GkkL/I1yqIUxShY8zJaglBuQiFP1mtwi3rUI3UuqFdSG1qjMcuiGWy8CKyLXaHwcOLXcmuVDGnjk7dQqomWREI2gsltr77SIJivjmb9Z5oqFpi9HD7KJ0YqHHxoIPh5Kv0TrfCiJBpifX4Rgz2wE6prlE2E5/yOVUvxnOADHUPQSekvWBBkGMOA/KGOuBbSzEp8XWGODsfirnuw21CtbNt9WLrXC/jbx11Aq5D7nz/cw+Ar8JwzWazr9RTBRG28SXVFlNB+34inufGNI3KmUNsKK6fOai/wuLUsx24CaLyWhefWoDghVtdp03oUL4JDHCdAeob0cBMpaXXfhWq0TPdiujZc9YZBPuIj462dnoarc/4GMwMBj/Qfg3sqRx9Ez4dI0+UtzYfxgheaHu1Aqd1Mq0oXIVsh3EZ+Gsbg3touhYB3CK5HWaFckQICo1oE24VeSR3nXNDlablpqCroixZTetFttqs101RNIyXWVY2Bd1U7zn8nBcr3+Ukr9bZOksnBO7+UH6IFQ9/8eczkhMJfJ3Rd4TRwY0GCFUH8tIfS750gnWk8xOtCB8NMoxMGwHNRDfEdcKSWiKAIQAhOa7l7CIoxqO13Q7Udeft7ZfHqNiEQfQjDrL64Cccl7RCJFe4ENQWg0aVMyOEfeWT3XoHPPCrZ1VySpnrEG5+n2yN1i8vlQbnen4hnCI/37+BCCEcmlMPvtdz8Y/k+PzDXJb/iGaqdNvi2lY/XVgIqaEV6lvtnT4GLBuBBEpdnUUTFRYQhY9a3bkXLEKZBLy/vTpwuumEE3n8QOCm12mne0j9izBNcD5TP7qpn+EYxyRZTPLlnMZhSlMp6jTIaU0t3KA1Z3cB16Y849VQaW8BO1d6ECD538HrOoM3UAuXvMmEf43Pb4B5MUn4fj2D/h7Hw5X+n5Ybsm/eIfvlSP1zjv+/upX+x/35/Oy/fwFCRniE' diff --git a/backend/src/appenlight/tests/test_integration.py b/backend/src/appenlight/tests/test_integration.py new file mode 100644 index 0000000..66a6f6f --- /dev/null +++ b/backend/src/appenlight/tests/test_integration.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ +import pytest +import json +from webtest import TestApp + + +@pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables', + 'default_application') +class TestIntegrationAPIReportsView(object): + def test_no_json_payload(self, base_app): + app = TestApp(base_app) + url_path = '/api/reports' + headers = {'x-appenlight-api-key': 'xxxx'} + res = app.post(url_path, {}, status=400, + headers=headers) + + def test_wrong_json_payload(self, base_app): + app = TestApp(base_app) + url_path = '/api/reports' + headers = {'x-appenlight-api-key': 'xxxx'} + res = app.post(url_path, {}, status=400, headers=headers) + + def test_correct_json_payload(self, base_app): + import appenlight.tests.payload_examples as payload_examples + app = TestApp(base_app) + url_path = '/api/reports' + headers = {'x-appenlight-api-key': 'xxxx'} + res = app.post_json(url_path, [payload_examples.PYTHON_PAYLOAD_0_5], + headers=headers) + + def test_json_payload_wrong_key(self, base_app): + import appenlight.tests.payload_examples as payload_examples + app = TestApp(base_app) + url_path = '/api/reports' + res = app.post(url_path, + json.dumps([payload_examples.PYTHON_PAYLOAD_0_5]), + status=403) + + +@pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables', + 'default_data', 'default_application') +class TestIntegrationRegistrationView(object): + def test_register_empty(self, base_app): + url_path = '/register' + app = TestApp(base_app) + resp = app.get('/') + cookies = resp.headers.getall('Set-Cookie') + cookie = None + for name, value in [c.split('=', 1) for c in cookies]: + if name == 'XSRF-TOKEN': + cookie = value.split(';')[0] + headers = {'X-XSRF-TOKEN': cookie} + res = app.post(url_path, + params={'user_name': '', + 'user_password': '', + 'email': ''}, + headers=headers) + assert 'This field is required.' in res + + def test_register_proper(self, base_app): + url_path = '/register' + app = TestApp(base_app) + resp = app.get('/') + cookies = resp.headers.getall('Set-Cookie') + cookie = None + for name, value in [c.split('=', 1) for c in cookies]: + if name == 'XSRF-TOKEN': + cookie = value.split(';')[0] + headers = {'X-XSRF-TOKEN': cookie} + res = app.post(url_path, + params={'user_name': 'user_foo', + 'user_password': 'passbar', + 'email': 'foobar@blablabla.com'}, + headers=headers, + status=302) + + +@pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables', + 'default_data', 'default_application') +class TestIntegrationRegistrationAuthTokenView(object): + + def test_create_application_bad(self, base_app): + url_path = '/applications' + app = TestApp(base_app) + headers = {'x-appenlight-auth-token': ''} + app.post_json(url_path, + params={'resource_name': 'user_foo'}, + headers=headers, status=403) + + def test_create_application_proper(self, base_app): + url_path = '/applications' + app = TestApp(base_app) + headers = {'x-appenlight-auth-token': '1234'} + app.post_json(url_path, + params={'resource_name': 'user_foo'}, + headers=headers, status=200) diff --git a/backend/src/appenlight/tests/test_unit.py b/backend/src/appenlight/tests/test_unit.py new file mode 100644 index 0000000..66e8ed0 --- /dev/null +++ b/backend/src/appenlight/tests/test_unit.py @@ -0,0 +1,1671 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import copy +import logging +import mock +import pyramid +import pytest +import sqlalchemy as sa +import webob + +from datetime import datetime +from pyramid import testing + + +from appenlight.models import DBSession +from appenlight.lib.ext_json import json + + +log = logging.getLogger(__name__) + + +class DummyContext(object): + pass + + +@pytest.mark.usefixtures('base_app') +class BasicTest(object): + pass + + +@pytest.mark.usefixtures('base_app') +class TestMigration(object): + def test_migration(self): + assert 1 == 1 + + +class TestAPIReports_0_4_Validation(object): + @pytest.mark.parametrize('dummy_json', ['', {}, [], None]) + def test_no_payload(self, dummy_json): + import colander + from appenlight.validators import ReportListSchema_0_4 + utcnow = datetime.utcnow() + schema = ReportListSchema_0_4().bind(utcnow=utcnow) + with pytest.raises(colander.Invalid): + schema.deserialize(dummy_json) + + def test_minimal_payload(self, report_04_schema): + dummy_json = [{}] + import colander + from appenlight.validators import ReportListSchema_0_4 + utcnow = datetime.utcnow() + schema = ReportListSchema_0_4().bind(utcnow=utcnow) + with pytest.raises(colander.Invalid): + schema.deserialize(dummy_json) + + def test_minimal_payload(self): + from appenlight.validators import ReportListSchema_0_4 + dummy_json = [{'report_details': [{}]}] + utcnow = datetime.utcnow() + schema = ReportListSchema_0_4().bind(utcnow=utcnow) + deserialized = schema.deserialize(dummy_json) + + expected_deserialization = [ + {'error_type': '', + 'language': 'unknown', + 'report_details': [ + {'username': '', + 'traceback': None, + 'extra': None, + 'frameinfo': None, + 'url': '', + 'ip': None, + 'start_time': utcnow, + 'group_string': None, + 'request': {}, + 'request_stats': None, + 'end_time': None, + 'request_id': '', + 'message': '', + 'slow_calls': [], + 'user_agent': ''}], + 'server': 'unknown', + 'occurences': 1, + 'priority': 5, + 'view_name': '', + 'client': 'unknown', + 'http_status': 200, + 'error': '', + 'tags': None} + ] + assert deserialized == expected_deserialization + + def test_full_payload(self): + import appenlight.tests.payload_examples as payload_examples + from appenlight.validators import ReportListSchema_0_4 + utcnow = datetime.utcnow() + schema = ReportListSchema_0_4().bind(utcnow=utcnow) + PYTHON_PAYLOAD = copy.deepcopy(payload_examples.PYTHON_PAYLOAD_0_4) + utcnow = datetime.utcnow() + PYTHON_PAYLOAD["tags"] = [("foo", 1), ("action", "test"), ("baz", 1.1), + ("date", + utcnow.strftime('%Y-%m-%dT%H:%M:%S.0'))] + dummy_json = [PYTHON_PAYLOAD] + + deserialized = schema.deserialize(dummy_json) + assert deserialized[0]['error'] == PYTHON_PAYLOAD['error'] + assert deserialized[0]['language'] == PYTHON_PAYLOAD['language'] + assert deserialized[0]['server'] == PYTHON_PAYLOAD['server'] + assert deserialized[0]['priority'] == PYTHON_PAYLOAD['priority'] + assert deserialized[0]['view_name'] == PYTHON_PAYLOAD['view_name'] + assert deserialized[0]['client'] == PYTHON_PAYLOAD['client'] + assert deserialized[0]['http_status'] == PYTHON_PAYLOAD['http_status'] + assert deserialized[0]['error'] == PYTHON_PAYLOAD['error'] + assert deserialized[0]['occurences'] == PYTHON_PAYLOAD['occurences'] + first_detail = deserialized[0]['report_details'][0] + payload_detail = PYTHON_PAYLOAD['report_details'][0] + assert first_detail['username'] == payload_detail['username'] + assert first_detail['traceback'] == payload_detail['traceback'] + assert first_detail['url'] == payload_detail['url'] + assert first_detail['ip'] == payload_detail['ip'] + assert first_detail['start_time'].strftime('%Y-%m-%dT%H:%M:%S.0') == \ + payload_detail['start_time'] + assert first_detail['ip'] == payload_detail['ip'] + assert first_detail['group_string'] is None + assert first_detail['request_stats'] == payload_detail['request_stats'] + assert first_detail['end_time'].strftime('%Y-%m-%dT%H:%M:%S.0') == \ + payload_detail['end_time'] + assert first_detail['request_id'] == payload_detail['request_id'] + assert first_detail['message'] == payload_detail['message'] + assert first_detail['user_agent'] == payload_detail['user_agent'] + slow_call = first_detail['slow_calls'][0] + expected_slow_call = payload_detail['slow_calls'][0] + assert slow_call['start'].strftime('%Y-%m-%dT%H:%M:%S.0') == \ + expected_slow_call['start'] + assert slow_call['end'].strftime('%Y-%m-%dT%H:%M:%S.0') == \ + expected_slow_call['end'] + assert slow_call['statement'] == expected_slow_call['statement'] + assert slow_call['parameters'] == expected_slow_call['parameters'] + assert slow_call['type'] == expected_slow_call['type'] + assert slow_call['subtype'] == expected_slow_call['subtype'] + assert slow_call['location'] == '' + assert deserialized[0]['tags'] == [ + ('foo', 1), ('action', 'test'), + ('baz', 1.1), ('date', utcnow.strftime('%Y-%m-%dT%H:%M:%S.0'))] + + +class TestSentryProto_7(object): + def test_log_payload(self): + import appenlight.tests.payload_examples as payload_examples + from appenlight.lib.enums import ParsedSentryEventType + from appenlight.lib.utils.sentry import parse_sentry_event + event_dict, event_type = parse_sentry_event( + payload_examples.SENTRY_LOG_PAYLOAD_7) + assert ParsedSentryEventType.LOG == event_type + assert event_dict['log_level'] == 'CRITICAL' + assert event_dict['message'] == 'TEST from django logging' + assert event_dict['namespace'] == 'testlogger' + assert event_dict['request_id'] == '9a6172f2e6d2444582f83a6c333d9cfb' + assert event_dict['server'] == 'ergo-virtual-machine' + assert event_dict['date'] == datetime.utcnow().date().strftime( + '%Y-%m-%dT%H:%M:%SZ') + tags = [('site', 'example.com'), + ('sys.argv', ["'manage.py'", "'runserver'"]), + ('price', 6), + ('tag', "'extra'"), + ('dupa', True), + ('project', 'sentry'), + ('sentry_culprit', 'testlogger in index'), + ('sentry_language', 'python'), + ('sentry_release', 'test')] + assert sorted(event_dict['tags']) == sorted(tags) + + def test_report_payload(self): + import appenlight.tests.payload_examples as payload_examples + from appenlight.lib.enums import ParsedSentryEventType + from appenlight.lib.utils.sentry import parse_sentry_event + utcnow = datetime.utcnow().date().strftime('%Y-%m-%dT%H:%M:%SZ') + event_dict, event_type = parse_sentry_event( + payload_examples.SENTRY_PYTHON_PAYLOAD_7) + assert ParsedSentryEventType.ERROR_REPORT == event_type + assert event_dict['client'] == 'sentry' + assert event_dict[ + 'error'] == 'Exception: test 500 ' \ + '\u0142\xf3\u201c\u0107\u201c\u0107\u017c\u0105' + assert event_dict['language'] == 'python' + assert event_dict['ip'] == '127.0.0.1' + assert event_dict['request_id'] == '9fae652c8c1c4d6a8eee09260f613a98' + assert event_dict['server'] == 'ergo-virtual-machine' + assert event_dict['start_time'] == utcnow + assert event_dict['url'] == 'http://127.0.0.1:8000/error' + assert event_dict['user_agent'] == 'Mozilla/5.0 (X11; Linux x86_64) ' \ + 'AppleWebKit/537.36 (KHTML, ' \ + 'like Gecko) Chrome/47.0.2526.106 ' \ + 'Safari/537.36' + assert event_dict['view_name'] == 'djangoapp.views in error' + tags = [('site', 'example.com'), ('sentry_release', 'test')] + assert sorted(event_dict['tags']) == sorted(tags) + extra = [('sys.argv', ["'manage.py'", "'runserver'"]), + ('project', 'sentry')] + assert sorted(event_dict['extra']) == sorted(extra) + request = event_dict['request'] + assert request['url'] == 'http://127.0.0.1:8000/error' + assert request['cookies'] == {'appenlight': 'X'} + assert request['data'] is None + assert request['method'] == 'GET' + assert request['query_string'] == '' + assert request['env'] == {'REMOTE_ADDR': '127.0.0.1', + 'SERVER_NAME': 'localhost', + 'SERVER_PORT': '8000'} + assert request['headers'] == { + 'Accept': 'text/html,application/xhtml+xml,' + 'application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Encoding': 'gzip, deflate, sdch', + 'Accept-Language': 'en-US,en;q=0.8,pl;q=0.6', + 'Connection': 'keep-alive', + 'Content-Length': '', + 'Content-Type': 'text/plain', + 'Cookie': 'appenlight=X', + 'Dnt': '1', + 'Host': '127.0.0.1:8000', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/47.0.2526.106 Safari/537.36'} + traceback = event_dict['traceback'] + assert traceback[0]['cline'] == 'response = wrapped_callback(request, ' \ + '*callback_args, **callback_kwargs)' + assert traceback[0]['file'] == 'django/core/handlers/base.py' + assert traceback[0]['fn'] == 'get_response' + assert traceback[0]['line'] == 111 + assert traceback[0]['module'] == 'django.core.handlers.base' + + assert traceback[1]['cline'] == "raise Exception(u'test 500 " \ + "\u0142\xf3\u201c\u0107\u201c\u0107" \ + "\u017c\u0105')" + assert traceback[1]['file'] == 'djangoapp/views.py' + assert traceback[1]['fn'] == 'error' + assert traceback[1]['line'] == 84 + assert traceback[1]['module'] == 'djangoapp.views' + assert sorted(traceback[1]['vars']) == sorted([ + ('c', + ''), + ('request', + ''), + ('conn', + '')]) + + +class TestAPIReports_0_5_Validation(object): + @pytest.mark.parametrize('dummy_json', ['', {}, [], None]) + def test_no_payload(self, dummy_json): + import colander + from appenlight.validators import ReportListSchema_0_5 + utcnow = datetime.utcnow() + schema = ReportListSchema_0_5().bind(utcnow=utcnow) + with pytest.raises(colander.Invalid): + schema.deserialize(dummy_json) + + def test_minimal_payload(self): + dummy_json = [{}] + import colander + from appenlight.validators import ReportListSchema_0_5 + utcnow = datetime.utcnow() + schema = ReportListSchema_0_5().bind(utcnow=utcnow) + with pytest.raises(colander.Invalid): + schema.deserialize(dummy_json) + + def test_minimal_payload(self): + dummy_json = [{'report_details': [{}]}] + from appenlight.validators import ReportListSchema_0_5 + utcnow = datetime.utcnow() + schema = ReportListSchema_0_5().bind(utcnow=utcnow) + + deserialized = schema.deserialize(dummy_json) + + expected_deserialization = [ + {'language': 'unknown', + 'server': 'unknown', + 'occurences': 1, + 'priority': 5, + 'view_name': '', + 'client': 'unknown', + 'http_status': 200, + 'error': '', + 'tags': None, + 'username': '', + 'traceback': None, + 'extra': None, + 'url': '', + 'ip': None, + 'start_time': utcnow, + 'group_string': None, + 'request': {}, + 'request_stats': None, + 'end_time': None, + 'request_id': '', + 'message': '', + 'slow_calls': [], + 'user_agent': '' + } + ] + assert deserialized == expected_deserialization + + def test_full_payload(self): + import appenlight.tests.payload_examples as payload_examples + from appenlight.validators import ReportListSchema_0_5 + PYTHON_PAYLOAD = copy.deepcopy(payload_examples.PYTHON_PAYLOAD_0_5) + utcnow = datetime.utcnow() + schema = ReportListSchema_0_5().bind(utcnow=utcnow) + PYTHON_PAYLOAD["tags"] = [("foo", 1), ("action", "test"), ("baz", 1.1), + ("date", + utcnow.strftime('%Y-%m-%dT%H:%M:%S.0'))] + dummy_json = [PYTHON_PAYLOAD] + deserialized = schema.deserialize(dummy_json)[0] + assert deserialized['error'] == PYTHON_PAYLOAD['error'] + assert deserialized['language'] == PYTHON_PAYLOAD['language'] + assert deserialized['server'] == PYTHON_PAYLOAD['server'] + assert deserialized['priority'] == PYTHON_PAYLOAD['priority'] + assert deserialized['view_name'] == PYTHON_PAYLOAD['view_name'] + assert deserialized['client'] == PYTHON_PAYLOAD['client'] + assert deserialized['http_status'] == PYTHON_PAYLOAD['http_status'] + assert deserialized['error'] == PYTHON_PAYLOAD['error'] + assert deserialized['occurences'] == PYTHON_PAYLOAD['occurences'] + assert deserialized['username'] == PYTHON_PAYLOAD['username'] + assert deserialized['traceback'] == PYTHON_PAYLOAD['traceback'] + assert deserialized['url'] == PYTHON_PAYLOAD['url'] + assert deserialized['ip'] == PYTHON_PAYLOAD['ip'] + assert deserialized['start_time'].strftime('%Y-%m-%dT%H:%M:%S.0') == \ + PYTHON_PAYLOAD['start_time'] + assert deserialized['ip'] == PYTHON_PAYLOAD['ip'] + assert deserialized['group_string'] is None + assert deserialized['request_stats'] == PYTHON_PAYLOAD['request_stats'] + assert deserialized['end_time'].strftime('%Y-%m-%dT%H:%M:%S.0') == \ + PYTHON_PAYLOAD['end_time'] + assert deserialized['request_id'] == PYTHON_PAYLOAD['request_id'] + assert deserialized['message'] == PYTHON_PAYLOAD['message'] + assert deserialized['user_agent'] == PYTHON_PAYLOAD['user_agent'] + assert deserialized['slow_calls'][0]['start'].strftime( + '%Y-%m-%dT%H:%M:%S.0') == PYTHON_PAYLOAD['slow_calls'][0][ + 'start'] + assert deserialized['slow_calls'][0]['end'].strftime( + '%Y-%m-%dT%H:%M:%S.0') == PYTHON_PAYLOAD['slow_calls'][0][ + 'end'] + assert deserialized['slow_calls'][0]['statement'] == \ + PYTHON_PAYLOAD['slow_calls'][0]['statement'] + assert deserialized['slow_calls'][0]['parameters'] == \ + PYTHON_PAYLOAD['slow_calls'][0]['parameters'] + assert deserialized['slow_calls'][0]['type'] == \ + PYTHON_PAYLOAD['slow_calls'][0]['type'] + assert deserialized['slow_calls'][0]['subtype'] == \ + PYTHON_PAYLOAD['slow_calls'][0]['subtype'] + assert deserialized['slow_calls'][0]['location'] == '' + assert deserialized['tags'] == [ + ('foo', 1), ('action', 'test'), + ('baz', 1.1), ('date', utcnow.strftime('%Y-%m-%dT%H:%M:%S.0'))] + + +@pytest.mark.usefixtures('log_schema') +class TestAPILogsValidation(object): + @pytest.mark.parametrize('dummy_json', ['', {}, [], None]) + def test_no_payload(self, dummy_json, log_schema): + import colander + + with pytest.raises(colander.Invalid): + log_schema.deserialize(dummy_json) + + def test_minimal_payload(self, log_schema): + dummy_json = [{}] + deserialized = log_schema.deserialize(dummy_json)[0] + expected = {'log_level': 'UNKNOWN', + 'namespace': '', + 'server': 'unknown', + 'request_id': '', + 'primary_key': None, + 'date': datetime.utcnow(), + 'message': '', + 'tags': None} + assert deserialized['log_level'] == expected['log_level'] + assert deserialized['message'] == expected['message'] + assert deserialized['namespace'] == expected['namespace'] + assert deserialized['request_id'] == expected['request_id'] + assert deserialized['server'] == expected['server'] + assert deserialized['tags'] == expected['tags'] + assert deserialized['primary_key'] == expected['primary_key'] + + def test_normal_payload(self, log_schema): + import appenlight.tests.payload_examples as payload_examples + deserialized = log_schema.deserialize(payload_examples.LOG_EXAMPLES)[0] + expected = payload_examples.LOG_EXAMPLES[0] + assert deserialized['log_level'] == expected['log_level'] + assert deserialized['message'] == expected['message'] + assert deserialized['namespace'] == expected['namespace'] + assert deserialized['request_id'] == expected['request_id'] + assert deserialized['server'] == expected['server'] + assert deserialized['date'].strftime('%Y-%m-%dT%H:%M:%S.%f') == \ + expected['date'] + assert deserialized['tags'][0][0] == "tag_name" + assert deserialized['tags'][0][1] == "tag_value" + assert deserialized['tags'][1][0] == "tag_name2" + assert deserialized['tags'][1][1] == 2 + + def test_normal_payload_date_without_microseconds(self, log_schema): + import appenlight.tests.payload_examples as payload_examples + LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES) + LOG_EXAMPLE[0]['date'] = datetime.utcnow().strftime( + '%Y-%m-%dT%H:%M:%S') + deserialized = log_schema.deserialize(LOG_EXAMPLE) + assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M:%S') == \ + LOG_EXAMPLE[0]['date'] + + def test_normal_payload_date_without_seconds(self, log_schema): + import appenlight.tests.payload_examples as payload_examples + LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES) + LOG_EXAMPLE[0]['date'] = datetime.utcnow().date().strftime( + '%Y-%m-%dT%H:%M') + deserialized = log_schema.deserialize(LOG_EXAMPLE) + assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M') == \ + LOG_EXAMPLE[0]['date'] + + def test_payload_empty_date(self, log_schema): + import appenlight.tests.payload_examples as payload_examples + LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES) + LOG_EXAMPLE[0]['date'] = None + deserialized = log_schema.deserialize(LOG_EXAMPLE) + assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M') is not None + + def test_payload_no_date(self, log_schema): + import appenlight.tests.payload_examples as payload_examples + LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES) + LOG_EXAMPLE[0].pop('date', None) + deserialized = log_schema.deserialize(LOG_EXAMPLE) + assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M') is not None + + +@pytest.mark.usefixtures('general_metrics_schema') +class TestAPIGeneralMetricsValidation(object): + @pytest.mark.parametrize('dummy_json', ['', {}, [], None]) + def test_no_payload(self, dummy_json, general_metrics_schema): + import colander + + with pytest.raises(colander.Invalid): + general_metrics_schema.deserialize(dummy_json) + + def test_minimal_payload(self, general_metrics_schema): + dummy_json = [{}] + deserialized = general_metrics_schema.deserialize(dummy_json)[0] + expected = {'namespace': '', + 'server_name': 'unknown', + 'tags': None, + 'timestamp': datetime.utcnow()} + assert deserialized['namespace'] == expected['namespace'] + assert deserialized['server_name'] == expected['server_name'] + assert deserialized['tags'] == expected['tags'] + + def test_normal_payload(self, general_metrics_schema): + import appenlight.tests.payload_examples as payload_examples + dummy_json = [payload_examples.METRICS_PAYLOAD] + deserialized = general_metrics_schema.deserialize(dummy_json)[0] + expected = {'namespace': 'some.monitor', + 'server_name': 'server.name', + 'tags': [('usage_foo', 15.5), ('usage_bar', 63)], + 'timestamp': datetime.utcnow()} + assert deserialized['namespace'] == expected['namespace'] + assert deserialized['server_name'] == expected['server_name'] + assert deserialized['tags'] == expected['tags'] + + +@pytest.mark.usefixtures('request_metrics_schema') +class TestAPIRequestMetricsValidation(object): + @pytest.mark.parametrize('dummy_json', ['', {}, [], None]) + def test_no_payload(self, dummy_json, request_metrics_schema): + import colander + + with pytest.raises(colander.Invalid): + print(request_metrics_schema.deserialize(dummy_json)) + + def test_normal_payload(self, request_metrics_schema): + import appenlight.tests.payload_examples as payload_examples + dummy_json = payload_examples.REQUEST_METRICS_EXAMPLES + deserialized = request_metrics_schema.deserialize(dummy_json)[0] + expected = {'metrics': [('dir/module:func', + {'custom': 0.0, + 'custom_calls': 0.0, + 'main': 0.01664, + 'nosql': 0.00061, + 'nosql_calls': 23.0, + 'remote': 0.0, + 'remote_calls': 0.0, + 'requests': 1, + 'sql': 0.00105, + 'sql_calls': 2.0, + 'tmpl': 0.0, + 'tmpl_calls': 0.0}), + ('SomeView.function', + {'custom': 0.0, + 'custom_calls': 0.0, + 'main': 0.647261, + 'nosql': 0.306554, + 'nosql_calls': 140.0, + 'remote': 0.0, + 'remote_calls': 0.0, + 'requests': 28, + 'sql': 0.0, + 'sql_calls': 0.0, + 'tmpl': 0.0, + 'tmpl_calls': 0.0})], + 'server': 'some.server.hostname', + 'timestamp': datetime.utcnow()} + assert deserialized['server'] == expected['server'] + metric = deserialized['metrics'][0] + expected_metric = expected['metrics'][0] + assert metric[0] == expected_metric[0] + assert sorted(metric[1].items()) == sorted(expected_metric[1].items()) + + +@pytest.mark.usefixtures('default_application') +@pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables') +class TestAPIReportsView(object): + def test_no_json_payload(self, default_application): + import colander + from appenlight.models.services.application import ApplicationService + from appenlight.views.api import reports_create + + context = DummyContext() + context.resource = ApplicationService.by_id(1) + request = testing.DummyRequest( + headers={'Content-Type': 'application/json'}) + request.unsafe_json_body = '' + request.context = context + route = mock.Mock() + route.name = 'api_reports' + request.matched_route = route + with pytest.raises(colander.Invalid): + response = reports_create(request) + + def test_single_proper_json_0_5_payload(self): + import appenlight.tests.payload_examples as payload_examples + from appenlight.views.api import reports_create + from appenlight.models.services.application import ApplicationService + from appenlight.models.report_group import ReportGroup + route = mock.Mock() + route.name = 'api_reports' + request = pyramid.threadlocal.get_current_request() + context = DummyContext() + context.resource = ApplicationService.by_id(1) + request.context = context + request.matched_route = route + PYTHON_PAYLOAD = payload_examples.PYTHON_PAYLOAD_0_5 + request.unsafe_json_body = [copy.deepcopy(PYTHON_PAYLOAD)] + reports_create(request) + query = DBSession.query(ReportGroup) + report = query.first() + assert query.count() == 1 + assert report.total_reports == 1 + + def test_grouping_0_5(self): + import appenlight.tests.payload_examples as payload_examples + from appenlight.views.api import reports_create + from appenlight.models.services.application import ApplicationService + from appenlight.models.report_group import ReportGroup + route = mock.Mock() + route.name = 'api_reports' + request = pyramid.threadlocal.get_current_request() + context = DummyContext() + context.resource = ApplicationService.by_id(1) + request.context = context + request.matched_route = route + PYTHON_PAYLOAD = payload_examples.PYTHON_PAYLOAD_0_5 + request.unsafe_json_body = [copy.deepcopy(PYTHON_PAYLOAD), + copy.deepcopy(PYTHON_PAYLOAD)] + reports_create(request) + query = DBSession.query(ReportGroup) + report = query.first() + assert query.count() == 1 + assert report.total_reports == 2 + + def test_grouping_different_reports_0_5(self): + import appenlight.tests.payload_examples as payload_examples + from appenlight.views.api import reports_create + from appenlight.models.services.application import ApplicationService + from appenlight.models.report_group import ReportGroup + route = mock.Mock() + route.name = 'api_reports' + request = pyramid.threadlocal.get_current_request() + context = DummyContext() + context.resource = ApplicationService.by_id(1) + request.context = context + request.matched_route = route + PYTHON_PAYLOAD = payload_examples.PYTHON_PAYLOAD_0_5 + PARSED_REPORT_404 = payload_examples.PARSED_REPORT_404 + request.unsafe_json_body = [copy.deepcopy(PYTHON_PAYLOAD), + copy.deepcopy(PARSED_REPORT_404)] + reports_create(request) + query = DBSession.query(ReportGroup) + report = query.first() + assert query.count() == 2 + assert report.total_reports == 1 + + +@pytest.mark.usefixtures('default_application') +@pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables') +class TestAirbrakeXMLView(object): + + def test_normal_payload_parsing(self): + import datetime + import defusedxml.ElementTree as ElementTree + import appenlight.tests.payload_examples as payload_examples + from appenlight.lib.utils.airbrake import parse_airbrake_xml + from appenlight.validators import ReportListSchema_0_5 + + context = DummyContext() + request = testing.DummyRequest( + headers={'Content-Type': 'application/xml'}) + request.context = context + request.context.possibly_public = False + root = ElementTree.fromstring(payload_examples.AIRBRAKE_RUBY_EXAMPLE) + request.context.airbrake_xml_etree = root + error_dict = parse_airbrake_xml(request) + schema = ReportListSchema_0_5().bind(utcnow=datetime.datetime.utcnow()) + deserialized_report = schema.deserialize([error_dict])[0] + assert deserialized_report['client'] == 'Airbrake Notifier' + assert deserialized_report['error'] == 'NameError: undefined local variable or method `sdfdfdf\' for #<#:0x00000002c53df0>' + assert deserialized_report['http_status'] == 500 + assert deserialized_report['language'] == 'unknown' + assert deserialized_report['message'] == '' + assert deserialized_report['occurences'] == 1 + assert deserialized_report['priority'] == 5 + d_request = deserialized_report['request'] + assert d_request['GET'] == {'test': '1234'} + assert d_request['action_dispatch.request.parameters'] == { + 'action': 'index', + 'controller': 'welcome', + 'test': '1234'} + assert deserialized_report['request_id'] == 'c11b2267f3ad8b00a1768cae35559fa1' + assert deserialized_report['server'] == 'ergo-desktop' + assert deserialized_report['traceback'][0] == { + 'cline': 'block in start_thread', + 'file': '/home/ergo/.rbenv/versions/1.9.3-p327/lib/ruby/1.9.1/webrick/server.rb', + 'fn': 'block in start_thread', + 'line': '191', + 'module': '', + 'vars': {}} + assert deserialized_report['traceback'][-1] == { + 'cline': '_app_views_welcome_index_html_erb___2570061166873166679_31748940', + 'file': '[PROJECT_ROOT]/app/views/welcome/index.html.erb', + 'fn': '_app_views_welcome_index_html_erb___2570061166873166679_31748940', + 'line': '3', + 'module': '', + 'vars': {}} + assert deserialized_report['url'] == 'http://0.0.0.0:3000/welcome/index?test=1234' + assert deserialized_report['view_name'] == 'welcome:index' + + def test_normal_payload_view(self): + import defusedxml.ElementTree as ElementTree + import appenlight.tests.payload_examples as payload_examples + + from appenlight.models.services.application import ApplicationService + from appenlight.views.api import airbrake_xml_compat + + context = DummyContext() + context.resource = ApplicationService.by_id(1) + request = testing.DummyRequest( + headers={'Content-Type': 'application/xml'}) + request.context = context + request.context.possibly_public = False + root = ElementTree.fromstring(payload_examples.AIRBRAKE_RUBY_EXAMPLE) + request.context.airbrake_xml_etree = root + route = mock.Mock() + route.name = 'api_airbrake' + request.matched_route = route + result = airbrake_xml_compat(request) + assert '' in result + + +@pytest.mark.usefixtures('default_application') +@pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables') +class TestAPILogView(object): + def test_no_json_payload(self, base_app): + import colander + from appenlight.models.services.application import ApplicationService + from appenlight.views.api import logs_create + + context = DummyContext() + context.resource = ApplicationService.by_id(1) + request = testing.DummyRequest( + headers={'Content-Type': 'application/json'}) + request.context = context + request.registry = base_app.registry + request.unsafe_json_body = '' + route = mock.Mock() + route.name = 'api_logs' + request.matched_route = route + with pytest.raises(colander.Invalid): + response = logs_create(request) + + def test_single_json_payload(self): + import appenlight.tests.payload_examples as payload_examples + from appenlight.models.log import Log + from appenlight.views.api import logs_create + from appenlight.models.services.application import ApplicationService + route = mock.Mock() + route.name = 'api_logs' + request = pyramid.threadlocal.get_current_request() + context = DummyContext() + context.resource = ApplicationService.by_id(1) + request.context = context + request.matched_route = route + request.unsafe_json_body = [copy.deepcopy( + payload_examples.LOG_EXAMPLES[0])] + logs_create(request) + query = DBSession.query(Log) + log = query.first() + assert query.count() == 1 + assert log.message == "OMG ValueError happened" + + def test_multiple_json_payload(self): + import appenlight.tests.payload_examples as payload_examples + from appenlight.models.log import Log + from appenlight.views.api import logs_create + from appenlight.models.services.application import ApplicationService + route = mock.Mock() + route.name = 'api_logs' + request = pyramid.threadlocal.get_current_request() + context = DummyContext() + context.resource = ApplicationService.by_id(1) + request.context = context + request.matched_route = route + LOG_PAYLOAD = payload_examples.LOG_EXAMPLES[0] + LOG_PAYLOAD2 = payload_examples.LOG_EXAMPLES[1] + request.unsafe_json_body = copy.deepcopy([LOG_PAYLOAD, LOG_PAYLOAD2]) + logs_create(request) + query = DBSession.query(Log).order_by(sa.asc(Log.log_id)) + assert query.count() == 2 + assert query[0].message == "OMG ValueError happened" + assert query[1].message == "OMG ValueError happened2" + + def test_public_key_rewriting(self): + import appenlight.tests.payload_examples as payload_examples + from appenlight.models.log import Log + from appenlight.views.api import logs_create + from appenlight.models.services.application import ApplicationService + route = mock.Mock() + route.name = 'api_logs' + request = pyramid.threadlocal.get_current_request() + context = DummyContext() + context.resource = ApplicationService.by_id(1) + request.context = context + request.matched_route = route + + LOG_PAYLOAD = copy.deepcopy(payload_examples.LOG_EXAMPLES[0]) + LOG_PAYLOAD2 = copy.deepcopy(payload_examples.LOG_EXAMPLES[1]) + LOG_PAYLOAD['primary_key'] = 'X2' + LOG_PAYLOAD2['primary_key'] = 'X2' + request.unsafe_json_body = [LOG_PAYLOAD, LOG_PAYLOAD2] + logs_create(request) + + query = DBSession.query(Log).order_by(sa.asc(Log.log_id)) + assert query.count() == 1 + assert query[0].message == "OMG ValueError happened2" + +@pytest.mark.usefixtures('default_application') +@pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables') +class TestAPIGeneralMetricsView(object): + def test_no_json_payload(self, base_app): + import colander + from appenlight.models.services.application import ApplicationService + from appenlight.views.api import general_metrics_create + route = mock.Mock() + route.name = 'api_general_metrics' + context = DummyContext() + context.resource = ApplicationService.by_id(1) + request = testing.DummyRequest( + headers={'Content-Type': 'application/json'}) + request.context = context + request.registry = base_app.registry + request.unsafe_json_body = '' + request.matched_route = route + with pytest.raises(colander.Invalid): + general_metrics_create(request) + + def test_single_json_payload(self): + import appenlight.tests.payload_examples as payload_examples + from appenlight.models.request_metric import Metric + from appenlight.views.api import general_metrics_create + from appenlight.models.services.application import ApplicationService + route = mock.Mock() + route.name = 'api_general_metric' + request = pyramid.threadlocal.get_current_request() + request.matched_route = route + context = DummyContext() + context.resource = ApplicationService.by_id(1) + request.context = context + request.unsafe_json_body = payload_examples.METRICS_PAYLOAD + general_metrics_create(request) + query = DBSession.query(Metric) + metric = query.first() + assert query.count() == 1 + assert metric.namespace == 'some.monitor' + + def test_multiple_json_payload(self): + import appenlight.tests.payload_examples as payload_examples + from appenlight.models.request_metric import Metric + from appenlight.views.api import general_metrics_create + from appenlight.models.services.application import ApplicationService + route = mock.Mock() + route.name = 'api_general_metrics' + request = pyramid.threadlocal.get_current_request() + request.matched_route = route + context = DummyContext() + context.resource = ApplicationService.by_id(1) + request.context = context + request.unsafe_json_body = [ + copy.deepcopy(payload_examples.METRICS_PAYLOAD), + copy.deepcopy(payload_examples.METRICS_PAYLOAD), + ] + general_metrics_create(request) + query = DBSession.query(Metric) + metric = query.first() + assert query.count() == 2 + assert metric.namespace == 'some.monitor' + + +class TestGroupingMessageReplacements(object): + def replace_default_repr_python(self): + test_str = ''' + ConnectionError: ConnectionError((, 'Connection to domain.gr timed out. (connect timeout=10)')) caused by: ConnectTimeoutError((, 'Connection to domain.gr timed out. (connect timeout=10)')) + ''' + regex = r'<(.*?) object at (.*?)>' + + +class TestRulesKeyGetter(object): + def test_default_dict_getter_top_key(self): + from appenlight.lib.rule import Rule + struct = { + "a": { + "b": 'b', + "c": { + "d": 'd', + "g": { + "h": 'h' + } + }, + "e": 'e' + }, + "f": 'f' + } + result = Rule.default_dict_struct_getter(struct, "a") + assert result == struct['a'] + + def test_default_dict_getter_sub_key(self): + from appenlight.lib.rule import Rule + struct = { + "a": { + "b": 'b', + "c": { + "d": 'd', + "g": { + "h": 'h' + } + }, + "e": 'e' + }, + "f": 'f' + } + result = Rule.default_dict_struct_getter(struct, 'a:b') + assert result == struct['a']['b'] + result = Rule.default_dict_struct_getter(struct, 'a:c:d') + assert result == struct['a']['c']['d'] + + def test_default_obj_getter_top_key(self): + from appenlight.lib.rule import Rule + class TestStruct(object): + def __init__(self, a, b): + self.a = a + self.b = b + + struct = TestStruct(a='a', + b=TestStruct(a='x', b='y')) + result = Rule.default_obj_struct_getter(struct, "a") + assert result == struct.a + + def test_default_obj_getter_sub_key(self): + from appenlight.lib.rule import Rule + class TestStruct(object): + def __init__(self, name, a, b): + self.name = name + self.a = a + self.b = b + + def __repr__(self): + return ''.format(self.name) + + c = TestStruct('c', a=5, b='z') + b = TestStruct('b', a=c, b='y') + struct = TestStruct('a', a='a', b=b) + result = Rule.default_obj_struct_getter(struct, 'b:b') + assert result == struct.b.b + result = Rule.default_obj_struct_getter(struct, 'b:a:b') + assert result == struct.b.a.b + + +@pytest.mark.usefixtures('report_type_matrix') +class TestRulesParsing(): + @pytest.mark.parametrize("op, struct_value, test_value, match_result", [ + ('eq', 500, 500, True), + ('eq', 600, 500, False), + ('eq', 300, 500, False), + ('eq', "300", 500, False), + ('eq', "600", 500, False), + ('eq', "500", 500, True), + ('ne', 500, 500, False), + ('ne', 600, 500, True), + ('ne', 300, 500, True), + ('ne', "300", 500, True), + ('ne', "600", 500, True), + ('ne', "500", 500, False), + ('ge', 500, 500, True), + ('ge', 600, 500, True), + ('ge', 499, 500, False), + ('gt', 499, 500, False), + ('gt', 500, 500, False), + ('gt', 501, 500, True), + ('le', 499, 500, True), + ('le', 500, 500, True), + ('le', 501, 500, False), + ('lt', 499, 500, True), + ('lt', 500, 500, False), + ('lt', 501, 500, False), + ]) + def test_single_op_int(self, op, struct_value, test_value, match_result, + report_type_matrix): + from appenlight.lib.rule import Rule + rule_config = { + "op": op, + "field": "http_status", + "value": test_value + } + rule = Rule(rule_config, report_type_matrix) + + data = { + "http_status": struct_value + } + assert rule.match(data) is match_result + + @pytest.mark.parametrize("op, struct_value, test_value, match_result", [ + ('ge', "500.01", 500, True), + ('ge', "500.01", 500.02, False), + ('le', "500.01", 500.02, True) + ]) + def test_single_op_float(self, op, struct_value, test_value, match_result, + report_type_matrix): + from appenlight.lib.rule import Rule + rule_config = { + "op": op, + "field": "duration", + "value": test_value + } + rule = Rule(rule_config, report_type_matrix) + + data = { + "duration": struct_value + } + assert rule.match(data) is match_result + + @pytest.mark.parametrize("op, struct_value, test_value, match_result", [ + ('contains', 'foo bar baz', 'foo', True), + ('contains', 'foo bar baz', 'bar', True), + ('contains', 'foo bar baz', 'dupa', False), + ('startswith', 'foo bar baz', 'foo', True), + ('startswith', 'foo bar baz', 'bar', False), + ('endswith', 'foo bar baz', 'baz', True), + ('endswith', 'foo bar baz', 'bar', False), + ]) + def test_single_op_string(self, op, struct_value, test_value, + match_result, report_type_matrix): + from appenlight.lib.rule import Rule + rule_config = { + "op": op, + "field": "error", + "value": test_value + } + rule = Rule(rule_config, report_type_matrix) + + data = { + "error": struct_value + } + assert rule.match(data) is match_result + + @pytest.mark.parametrize("field, value, s_type", [ + ('field_unicode', 500, str), + ('field_unicode', 500.0, str), + ('field_unicode', "500", str), + ('field_int', "500", int), + ('field_int', 500, int), + ('field_int', 500.0, int), + ('field_float', "500", float), + ('field_float', 500, float), + ('field_float', 500.0, float), + ]) + def test_type_normalization(self, field, value, s_type): + from appenlight.lib.rule import Rule + type_matrix = { + 'field_unicode': {"type": 'unicode'}, + 'field_float': {"type": 'float'}, + 'field_int': {"type": 'int'}, + } + + rule = Rule({}, type_matrix) + n_value = rule.normalized_type(field, value) + assert isinstance(n_value, s_type) is True + + +@pytest.mark.usefixtures('report_type_matrix') +class TestNestedRuleParsing(): + @pytest.mark.parametrize("data, result", [ + ({"http_status": 501, "group": {"priority": 7, "occurences": 11}}, + True), + ({"http_status": 101, "group": {"priority": 7, "occurences": 11}}, + True), + ({"http_status": 500, "group": {"priority": 1, "occurences": 1}}, + True), + ({"http_status": 101, "group": {"priority": 3, "occurences": 11}}, + False), + ]) + def test_nested_OR_AND_rule(self, data, result, report_type_matrix): + from appenlight.lib.rule import Rule + rule_config = { + "field": "__OR__", + "rules": [ + { + "field": "__AND__", + "rules": [ + { + "op": "ge", + "field": "group:occurences", + "value": "10" + }, + { + "op": "ge", + "field": "group:priority", + "value": "4" + } + ] + }, + { + "op": "eq", + "field": "http_status", + "value": "500" + } + ] + } + + rule = Rule(rule_config, report_type_matrix) + assert rule.match(data) is result + + @pytest.mark.parametrize("data, result", [ + ({"http_status": 501, "group": {"priority": 7, "occurences": 11}}, + True), + ({"http_status": 101, "group": {"priority": 7, "occurences": 11}}, + True), + ({"http_status": 500, "group": {"priority": 1, "occurences": 1}}, + True), + ({"http_status": 101, "group": {"priority": 3, "occurences": 1}}, + False), + ]) + def test_nested_OR_OR_rule(self, data, result, report_type_matrix): + from appenlight.lib.rule import Rule + rule_config = { + "field": "__OR__", + "rules": [ + {"field": "__OR__", + "rules": [ + {"op": "ge", + "field": "group:occurences", + "value": "10" + }, + {"op": "ge", + "field": "group:priority", + "value": "4" + } + ] + }, + {"op": "eq", + "field": "http_status", + "value": "500" + } + ] + } + + rule = Rule(rule_config, report_type_matrix) + assert rule.match(data) is result + + @pytest.mark.parametrize("data, result", [ + ({"http_status": 500, "group": {"priority": 7, "occurences": 11}}, + True), + ({"http_status": 101, "group": {"priority": 7, "occurences": 11}}, + False), + ({"http_status": 500, "group": {"priority": 1, "occurences": 1}}, + False), + ({"http_status": 101, "group": {"priority": 3, "occurences": 1}}, + False), + ]) + def test_nested_AND_AND_rule(self, data, result, report_type_matrix): + from appenlight.lib.rule import Rule + rule_config = { + "field": "__AND__", + "rules": [ + {"field": "__AND__", + "rules": [ + {"op": "ge", + "field": "group:occurences", + "value": "10" + }, + {"op": "ge", + "field": "group:priority", + "value": "4" + }] + }, + {"op": "eq", + "field": "http_status", + "value": "500" + } + ] + } + + rule = Rule(rule_config, report_type_matrix) + assert rule.match(data) is result + + @pytest.mark.parametrize("data, result", [ + ({"http_status": 500, "group": {"priority": 7, "occurences": 11}, + "url_path": '/test/register', "error": "foo test bar"}, True), + ({"http_status": 500, "group": {"priority": 7, "occurences": 11}, + "url_path": '/test/register', "error": "foo INVALID bar"}, False), + ]) + def test_nested_AND_AND_AND_rule(self, data, result, report_type_matrix): + from appenlight.lib.rule import Rule + rule_config = { + "field": "__AND__", + "rules": [ + {"field": "__AND__", + "rules": [ + {"op": "ge", + "field": "group:occurences", + "value": "10" + }, + {"field": "__AND__", + "rules": [ + {"op": "endswith", + "field": "url_path", + "value": "register"}, + {"op": "contains", + "field": "error", + "value": "test"}]}] + }, + {"op": "eq", + "field": "http_status", + "value": "500" + } + ] + } + + rule = Rule(rule_config, report_type_matrix) + assert rule.match(data) is result + + @pytest.mark.parametrize("data, result", [ + ({"http_status": 500, "group": {"priority": 7, "occurences": 11}, + "url_path": 6, "error": 3}, False), + ({"http_status": 500, "group": {"priority": 7, "occurences": 11}, + "url_path": '/test/register', "error": "foo INVALID bar"}, True), + ]) + def test_nested_AND_AND_OR_rule(self, data, result, report_type_matrix): + from appenlight.lib.rule import Rule + rule_config = { + "field": "__AND__", + "rules": [ + {"field": "__AND__", + "rules": [ + {"op": "ge", + "field": "group:occurences", + "value": "10" + }, + {"field": "__OR__", + "rules": [ + {"op": "endswith", + "field": "url_path", + "value": "register" + }, + {"op": "contains", + "field": "error", + "value": "test" + }]}] + }, + {"op": "eq", + "field": "http_status", + "value": "500" + } + ] + } + + rule = Rule(rule_config, report_type_matrix) + assert rule.match(data) is result + + @pytest.mark.parametrize("op, field, value, should_fail", [ + ('eq', 'http_status', "1", False), + ('ne', 'http_status', "1", False), + ('ne', 'http_status', "foo", True), + ('startswith', 'http_status', "1", True), + ('eq', 'group:priority', "1", False), + ('ne', 'group:priority', "1", False), + ('ge', 'group:priority', "1", False), + ('le', 'group:priority', "1", False), + ('startswith', 'group:priority', "1", True), + ('eq', 'url_domain', "1", False), + ('ne', 'url_domain', "1", False), + ('startswith', 'url_domain', "1", False), + ('endswith', 'url_domain', "1", False), + ('contains', 'url_domain', "1", False), + ('ge', 'url_domain', "1", True), + ('eq', 'url_path', "1", False), + ('ne', 'url_path', "1", False), + ('startswith', 'url_path', "1", False), + ('endswith', 'url_path', "1", False), + ('contains', 'url_path', "1", False), + ('ge', 'url_path', "1", True), + ('eq', 'error', "1", False), + ('ne', 'error', "1", False), + ('startswith', 'error', "1", False), + ('endswith', 'error', "1", False), + ('contains', 'error', "1", False), + ('ge', 'error', "1", True), + ('ge', 'url_path', "1", True), + ('eq', 'tags:server_name', "1", False), + ('ne', 'tags:server_name', "1", False), + ('startswith', 'tags:server_name', "1", False), + ('endswith', 'tags:server_name', "1", False), + ('contains', 'tags:server_name', "1", False), + ('ge', 'tags:server_name', "1", True), + ('contains', 'traceback', "1", False), + ('ge', 'traceback', "1", True), + ('eq', 'group:occurences', "1", False), + ('ne', 'group:occurences', "1", False), + ('ge', 'group:occurences', "1", False), + ('le', 'group:occurences', "1", False), + ('contains', 'group:occurences', "1", True), + ]) + def test_rule_validation(self, op, field, value, should_fail, + report_type_matrix): + import colander + from appenlight.validators import build_rule_schema + rule_config = { + "op": op, + "field": field, + "value": value + } + + schema = build_rule_schema(rule_config, report_type_matrix) + if should_fail: + with pytest.raises(colander.Invalid): + schema.deserialize(rule_config) + else: + schema.deserialize(rule_config) + + def test_nested_proper_rule_validation(self, report_type_matrix): + from appenlight.validators import build_rule_schema + rule_config = { + "field": "__AND__", + "rules": [ + { + "field": "__AND__", + "rules": [ + { + "op": "ge", + "field": "group:occurences", + "value": "10" + }, + { + "field": "__OR__", + "rules": [ + { + "op": "endswith", + "field": "url_path", + "value": "register" + }, + { + "op": "contains", + "field": "error", + "value": "test" + } + ] + } + ] + }, + { + "op": "eq", + "field": "http_status", + "value": "500" + } + ] + } + + schema = build_rule_schema(rule_config, report_type_matrix) + deserialized = schema.deserialize(rule_config) + + def test_nested_bad_rule_validation(self, report_type_matrix): + import colander + from appenlight.validators import build_rule_schema + rule_config = { + "field": "__AND__", + "rules": [ + { + "field": "__AND__", + "rules": [ + { + "op": "ge", + "field": "group:occurences", + "value": "10" + }, + { + "field": "__OR__", + "rules": [ + { + "op": "gt", + "field": "url_path", + "value": "register" + }, + { + "op": "contains", + "field": "error", + "value": "test" + } + ] + } + ] + }, + { + "op": "eq", + "field": "http_status", + "value": "500" + } + ] + } + + schema = build_rule_schema(rule_config, report_type_matrix) + with pytest.raises(colander.Invalid): + deserialized = schema.deserialize(rule_config) + + def test_config_manipulator(self): + from appenlight.lib.rule import Rule + type_matrix = { + 'a': {"type": 'int', + "ops": ('eq', 'ne', 'ge', 'le',)}, + 'b': {"type": 'int', + "ops": ('eq', 'ne', 'ge', 'le',)}, + } + rule_config = { + "field": "__OR__", + "rules": [ + { + "field": "__OR__", + "rules": [ + { + "op": "ge", + "field": "a", + "value": "10" + } + ] + }, + { + "op": "eq", + "field": "b", + "value": "500" + } + ] + } + + def rule_manipulator(rule): + if 'value' in rule.config: + rule.config['value'] = "1" + + rule = Rule(rule_config, type_matrix, + config_manipulator=rule_manipulator) + rule.match({"a": 1, + "b": "2"}) + assert rule.config['rules'][0]['rules'][0]['value'] == "1" + assert rule.config['rules'][1]['value'] == "1" + assert rule.type_matrix["b"]['type'] == "int" + + def test_dynamic_config_manipulator(self): + from appenlight.lib.rule import Rule + rule_config = { + "field": "__OR__", + "rules": [ + { + "field": "__OR__", + "rules": [ + { + "op": "ge", + "field": "a", + "value": "10" + } + ] + }, + { + "op": "eq", + "field": "b", + "value": "500" + } + ] + } + + def rule_manipulator(rule): + rule.type_matrix = { + 'a': {"type": 'int', + "ops": ('eq', 'ne', 'ge', 'le',)}, + 'b': {"type": 'unicode', + "ops": ('eq', 'ne', 'ge', 'le',)}, + } + + if 'value' in rule.config: + if rule.config['field'] == 'a': + rule.config['value'] = "1" + elif rule.config['field'] == 'b': + rule.config['value'] = "2" + + rule = Rule(rule_config, {}, + config_manipulator=rule_manipulator) + rule.match({"a": 11, + "b": "55"}) + assert rule.config['rules'][0]['rules'][0]['value'] == "1" + assert rule.config['rules'][1]['value'] == "2" + assert rule.type_matrix["b"]['type'] == "unicode" + + +@pytest.mark.usefixtures('base_app', 'with_migrations') +class TestViewsWithForms(object): + def test_bad_csrf(self): + from appenlight.forms import CSRFException + from appenlight.views.index import register + post_data = {'dupa': 'dupa'} + request = testing.DummyRequest(post=post_data) + request.POST = webob.multidict.MultiDict(request.POST) + with pytest.raises(CSRFException): + register(request) + + def test_proper_csrf(self): + from appenlight.views.index import register + request = pyramid.threadlocal.get_current_request() + post_data = {'dupa': 'dupa', + 'csrf_token': request.session.get_csrf_token()} + request = testing.DummyRequest(post=post_data) + request.POST = webob.multidict.MultiDict(request.POST) + result = register(request) + assert result['form'].errors['email'][0] == 'This field is required.' + + +@pytest.mark.usefixtures('base_app', 'with_migrations', 'default_data') +class TestRegistration(object): + def test_invalid_form(self): + from appenlight.views.index import register + request = pyramid.threadlocal.get_current_request() + post_data = {'user_name': '', + 'user_password': '', + 'email': '', + 'csrf_token': request.session.get_csrf_token()} + request = testing.DummyRequest(post=post_data) + request.POST = webob.multidict.MultiDict(request.POST) + result = register(request) + assert result['form'].errors['user_name'][0] == \ + 'This field is required.' + + def test_valid_form(self): + from appenlight.views.index import register + from ziggurat_foundations.models.services.user import UserService + request = pyramid.threadlocal.get_current_request() + post_data = {'user_name': 'foo', + 'user_password': 'barr', + 'email': 'test@test.foo', + 'csrf_token': request.session.get_csrf_token()} + request = testing.DummyRequest(post=post_data) + request.add_flash_to_headers = mock.Mock() + request.POST = webob.multidict.MultiDict(request.POST) + assert UserService.by_user_name('foo') is None + register(request) + user = UserService.by_user_name('foo') + assert user.user_name == 'foo' + assert len(user.user_password) == 60 + + +@pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables', + 'default_user') +class TestApplicationCreation(object): + def test_wrong_data(self): + import appenlight.views.applications as applications + from ziggurat_foundations.models.services.user import UserService + request = pyramid.threadlocal.get_current_request() + request.user = UserService.by_user_name('testuser') + request.unsafe_json_body = {} + request.headers['X-XSRF-TOKEN'] = request.session.get_csrf_token() + response = applications.application_create(request) + assert response.code == 422 + + def test_proper_data(self): + import appenlight.views.applications as applications + from ziggurat_foundations.models.services.user import UserService + + request = pyramid.threadlocal.get_current_request() + request.user = UserService.by_user_name('testuser') + request.unsafe_json_body = {"resource_name": "app name", + "domains": "foo"} + request.headers['X-XSRF-TOKEN'] = request.session.get_csrf_token() + app_dict = applications.application_create(request) + assert app_dict['public_key'] is not None + assert app_dict['api_key'] is not None + assert app_dict['resource_name'] == 'app name' + assert app_dict['owner_group_id'] is None + assert app_dict['resource_id'] is not None + assert app_dict['default_grouping'] == 'url_traceback' + assert app_dict['possible_permissions'] == ('view', 'update_reports') + assert app_dict['slow_report_threshold'] == 10 + assert app_dict['owner_user_name'] == 'testuser' + assert app_dict['owner_user_id'] == request.user.id + assert app_dict['domains'] is 'foo' + assert app_dict['postprocessing_rules'] == [] + assert app_dict['error_report_threshold'] == 10 + assert app_dict['allow_permanent_storage'] is False + assert app_dict['resource_type'] == 'application' + assert app_dict['current_permissions'] == [] + + +@pytest.mark.usefixtures('default_application') +@pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables') +class TestAPISentryView(object): + def test_no_payload(self, default_application): + import colander + from appenlight.models.services.application import ApplicationService + from appenlight.views.api import sentry_compat + from appenlight.lib.request import JSONException + + context = DummyContext() + context.resource = ApplicationService.by_id(1) + request = testing.DummyRequest( + headers={'Content-Type': 'application/json'}) + request.unsafe_json_body = '' + request.context = context + route = mock.Mock() + route.name = 'api_sentry' + request.matched_route = route + with pytest.raises(JSONException): + sentry_compat(request) + + def test_java_client_payload(self): + from appenlight.views.api import sentry_compat + from appenlight.models.services.application import ApplicationService + from appenlight.models.report_group import ReportGroup + route = mock.Mock() + route.name = 'api_sentry' + request = pyramid.threadlocal.get_current_request() + context = DummyContext() + context.resource = ApplicationService.by_id(1) + request.context = context + request.matched_route = route + request.body = b'eJy1UmFr2zAQ/S0T+7BCLOzYThp/C6xjG6SDLd/GCBf57Ki' \ + b'RJSHJJiXkv+/UlC7p2kAZA33Ru6f33t1pz3BAHVayZhWr87' \ + b'JMs+I6q3MsrifFep2vc1iXM1HMpgBTNmIdeg8tEvlmJ9AGa' \ + b'fQ7goOkQoDOUmGcZpMkLZO0WGZFRadMiaHIR1EVnTMu3k3b' \ + b'oiMgqJrXpgOpOVjLLTiPkWAVhMa4jih3MAAholfWyUDAksz' \ + b'm1iopICbg8fWH52B8VWXZVYwHrWfV/jBipD2gW2no8CFMa5' \ + b'JButCDSjoQG6mR6LgLDojPPn/7sbydL25ep34HGl+y3DiE+' \ + b'lH0xXBXjMzFBsXW99SS7pWKYXRw91zqgK4BgZ4/DZVVP/cs' \ + b'3NuzSZPfAKqP2Cdj4tw7U/cKH0fEFeiWQFqE2FIHAmMPjaN' \ + b'Y/kHvbzY/JqdHUq9o/KxqQHkcsabX4piDuT4aK+pXG1ZNi/' \ + b'IwOpEyruXC1LiB3vPO3BmOOxTUCIqv5LIg5H12oh9cf0l+P' \ + b'MvP5P8kddgoFIEvMGzM5cRSD2aLJ6qTdHKm6nv9pPcRFba0' \ + b'Kd0eleeCFuGN+9JZ9TaXIn/V5JYMBvxXg3L6PwzSE4dkfOb' \ + b'w7CtfWmP85SdCs8OvA53fUV19cg==' + sentry_compat(request) + query = DBSession.query(ReportGroup) + report = query.first() + assert query.count() == 1 + assert report.total_reports == 1 + + def test_ruby_client_payload(self): + from appenlight.views.api import sentry_compat + from appenlight.models.services.application import ApplicationService + from appenlight.models.report_group import ReportGroup + from appenlight.tests.payload_examples import SENTRY_RUBY_ENCODED + route = mock.Mock() + route.name = 'api_sentry' + request = testing.DummyRequest( + headers={'Content-Type': 'application/octet-stream', + 'User-Agent': 'sentry-ruby/1.0.0', + 'X-Sentry-Auth': 'Sentry sentry_version=5, ' + 'sentry_client=raven-ruby/1.0.0, ' + 'sentry_timestamp=1462378483, ' + 'sentry_key=xxx, sentry_secret=xxx' + }) + context = DummyContext() + context.resource = ApplicationService.by_id(1) + request.context = context + request.matched_route = route + request.body = SENTRY_RUBY_ENCODED + sentry_compat(request) + query = DBSession.query(ReportGroup) + report = query.first() + assert query.count() == 1 + assert report.total_reports == 1 + + def test_python_client_decoded_payload(self): + from appenlight.views.api import sentry_compat + from appenlight.models.services.application import ApplicationService + from appenlight.models.report_group import ReportGroup + from appenlight.tests.payload_examples import SENTRY_PYTHON_PAYLOAD_7 + route = mock.Mock() + route.name = 'api_sentry' + request = pyramid.threadlocal.get_current_request() + context = DummyContext() + context.resource = ApplicationService.by_id(1) + request.context = context + request.matched_route = route + request.body = json.dumps(SENTRY_PYTHON_PAYLOAD_7).encode('utf8') + sentry_compat(request) + query = DBSession.query(ReportGroup) + report = query.first() + assert query.count() == 1 + assert report.total_reports == 1 + + def test_python_client_encoded_payload(self): + from appenlight.views.api import sentry_compat + from appenlight.models.services.application import ApplicationService + from appenlight.models.report_group import ReportGroup + from appenlight.tests.payload_examples import SENTRY_PYTHON_ENCODED + route = mock.Mock() + route.name = 'api_sentry' + request = testing.DummyRequest( + headers={'Content-Type': 'application/octet-stream', + 'Content-Encoding': 'deflate', + 'User-Agent': 'sentry-ruby/1.0.0', + 'X-Sentry-Auth': 'Sentry sentry_version=5, ' + 'sentry_client=raven-ruby/1.0.0, ' + 'sentry_timestamp=1462378483, ' + 'sentry_key=xxx, sentry_secret=xxx' + }) + context = DummyContext() + context.resource = ApplicationService.by_id(1) + request.context = context + request.matched_route = route + request.body = SENTRY_PYTHON_ENCODED + sentry_compat(request) + query = DBSession.query(ReportGroup) + report = query.first() + assert query.count() == 1 + assert report.total_reports == 1 diff --git a/backend/src/appenlight/validators.py b/backend/src/appenlight/validators.py new file mode 100644 index 0000000..23f6362 --- /dev/null +++ b/backend/src/appenlight/validators.py @@ -0,0 +1,758 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import datetime + +import colander +from colander import null + +# those keywords are here so we can distingush between searching for tags and +# normal properties of reports/logs +accepted_search_params = ['resource', + 'request_id', + 'start_date', + 'end_date', + 'page', + 'min_occurences', + 'http_status', + 'priority', + 'error', + 'url_path', + 'url_domain', + 'report_status', + 'min_duration', + 'max_duration', + 'message', + 'level', + 'namespace'] + + +@colander.deferred +def deferred_utcnow(node, kw): + return kw['utcnow'] + + +def lowercase_preparer(input_data): + """ + Transforms a list of string entries to lowercase + Used in search query validation + """ + if not input_data: + return input_data + return [x.lower() for x in input_data] + + +def shortener_factory(cutoff_size=32): + """ + Limits the input data to specific character count + :arg cutoff_cutoff_size How much characters to store + + """ + + def shortener(input_data): + if not input_data: + return input_data + else: + if isinstance(input_data, str): + return input_data[:cutoff_size] + else: + return input_data + + return shortener + + +def cast_to_unicode_or_null(value): + if value is not colander.null: + return str(value) + return None + + +class NonTZDate(colander.DateTime): + """ Returns null for incorrect date format - also removes tz info""" + + def deserialize(self, node, cstruct): + # disabled for now + # if cstruct and isinstance(cstruct, str): + # if ':' not in cstruct: + # cstruct += ':0.0' + # if '.' not in cstruct: + # cstruct += '.0' + value = super(NonTZDate, self).deserialize(node, cstruct) + if value: + return value.replace(tzinfo=None) + return value + + +class UnknownType(object): + """ + Universal type that will accept a deserialized JSON object and store it unaltered + """ + + def serialize(self, node, appstruct): + if appstruct is null: + return null + return appstruct + + def deserialize(self, node, cstruct): + if cstruct is null: + return null + return cstruct + + def cstruct_children(self): + return [] + + +# SLOW REPORT SCHEMA + +def rewrite_type(input_data): + """ + Fix for legacy appenlight clients + """ + if input_data == 'remote_call': + return 'remote' + return input_data + + +class ExtraTupleSchema(colander.TupleSchema): + name = colander.SchemaNode(colander.String(), + validator=colander.Length(1, 64)) + value = colander.SchemaNode(UnknownType(), + preparer=shortener_factory(512), + missing=None) + + +class ExtraSchemaList(colander.SequenceSchema): + tag = ExtraTupleSchema() + missing = None + + +class TagsTupleSchema(colander.TupleSchema): + name = colander.SchemaNode(colander.String(), + validator=colander.Length(1, 128)) + value = colander.SchemaNode(UnknownType(), + preparer=shortener_factory(128), + missing=None) + + +class TagSchemaList(colander.SequenceSchema): + tag = TagsTupleSchema() + missing = None + + +class NumericTagsTupleSchema(colander.TupleSchema): + name = colander.SchemaNode(colander.String(), + validator=colander.Length(1, 128)) + value = colander.SchemaNode(colander.Float(), missing=0) + + +class NumericTagSchemaList(colander.SequenceSchema): + tag = NumericTagsTupleSchema() + missing = None + + +class SlowCallSchema(colander.MappingSchema): + """ + Validates slow call format in slow call list + """ + start = colander.SchemaNode(NonTZDate()) + end = colander.SchemaNode(NonTZDate()) + statement = colander.SchemaNode(colander.String(), missing='') + parameters = colander.SchemaNode(UnknownType(), missing=None) + type = colander.SchemaNode( + colander.String(), + preparer=rewrite_type, + validator=colander.OneOf( + ['tmpl', 'sql', 'nosql', 'remote', 'unknown', 'custom']), + missing='unknown') + subtype = colander.SchemaNode(colander.String(), + validator=colander.Length(1, 16), + missing='unknown') + location = colander.SchemaNode(colander.String(), + validator=colander.Length(1, 255), + missing='') + + +def limited_date(node, value): + """ checks to make sure that the value is not older/newer than 2h """ + hours = 2 + min_time = datetime.datetime.utcnow() - datetime.timedelta(hours=72) + max_time = datetime.datetime.utcnow() + datetime.timedelta(hours=2) + if min_time > value: + msg = '%r is older from current UTC time by ' + str(hours) + ' hours.' + msg += ' Ask administrator to enable permanent logging for ' \ + 'your application to store logs with dates in past.' + raise colander.Invalid(node, msg % value) + if max_time < value: + msg = '%r is newer from current UTC time by ' + str(hours) + ' hours' + msg += ' Ask administrator to enable permanent logging for ' \ + 'your application to store logs with dates in future.' + raise colander.Invalid(node, msg % value) + + +class SlowCallListSchema(colander.SequenceSchema): + """ + Validates list of individual slow calls + """ + slow_call = SlowCallSchema() + + +class RequestStatsSchema(colander.MappingSchema): + """ + Validates format of requests statistics dictionary + """ + main = colander.SchemaNode(colander.Float(), validator=colander.Range(0), + missing=0) + sql = colander.SchemaNode(colander.Float(), validator=colander.Range(0), + missing=0) + nosql = colander.SchemaNode(colander.Float(), validator=colander.Range(0), + missing=0) + remote = colander.SchemaNode(colander.Float(), validator=colander.Range(0), + missing=0) + tmpl = colander.SchemaNode(colander.Float(), validator=colander.Range(0), + missing=0) + custom = colander.SchemaNode(colander.Float(), validator=colander.Range(0), + missing=0) + sql_calls = colander.SchemaNode(colander.Float(), + validator=colander.Range(0), + missing=0) + nosql_calls = colander.SchemaNode(colander.Float(), + validator=colander.Range(0), + missing=0) + remote_calls = colander.SchemaNode(colander.Float(), + validator=colander.Range(0), + missing=0) + tmpl_calls = colander.SchemaNode(colander.Float(), + validator=colander.Range(0), + missing=0) + custom_calls = colander.SchemaNode(colander.Float(), + validator=colander.Range(0), + missing=0) + + +class FrameInfoVarSchema(colander.SequenceSchema): + """ + Validates format of frame variables of a traceback + """ + vars = colander.SchemaNode(UnknownType(), + validator=colander.Length(2, 2)) + + +class FrameInfoSchema(colander.MappingSchema): + """ + Validates format of a traceback line + """ + cline = colander.SchemaNode(colander.String(), missing='') + module = colander.SchemaNode(colander.String(), missing='') + line = colander.SchemaNode(colander.String(), missing='') + file = colander.SchemaNode(colander.String(), missing='') + fn = colander.SchemaNode(colander.String(), missing='') + vars = FrameInfoVarSchema() + + +class FrameInfoListSchema(colander.SequenceSchema): + """ + Validates format of list of traceback lines + """ + frame = colander.SchemaNode(UnknownType()) + + +class ReportDetailBaseSchema(colander.MappingSchema): + """ + Validates format of report - ie. request parameters and stats for a request in report group + """ + username = colander.SchemaNode(colander.String(), + preparer=[shortener_factory(255), + lambda x: x or ''], + missing='') + request_id = colander.SchemaNode(colander.String(), + preparer=shortener_factory(40), + missing='') + url = colander.SchemaNode(colander.String(), + preparer=shortener_factory(1024), missing='') + ip = colander.SchemaNode(colander.String(), preparer=shortener_factory(39), + missing=None) + start_time = colander.SchemaNode(NonTZDate(), validator=limited_date, + missing=deferred_utcnow) + end_time = colander.SchemaNode(NonTZDate(), validator=limited_date, + missing=None) + user_agent = colander.SchemaNode(colander.String(), + preparer=[shortener_factory(512), + lambda x: x or ''], + missing='') + message = colander.SchemaNode(colander.String(), + preparer=shortener_factory(2048), + missing='') + group_string = colander.SchemaNode(colander.String(), + validator=colander.Length(1, 512), + missing=None) + request_stats = RequestStatsSchema(missing=None) + request = colander.SchemaNode(colander.Mapping(unknown='preserve'), + missing={}) + traceback = FrameInfoListSchema(missing=None) + slow_calls = SlowCallListSchema(missing=[]) + extra = ExtraSchemaList() + + +class ReportDetailSchema_0_4(ReportDetailBaseSchema): + frameinfo = FrameInfoListSchema(missing=None) + + +class ReportDetailSchema_0_5(ReportDetailBaseSchema): + pass + + +class ReportDetailListSchema(colander.SequenceSchema): + """ + Validates format of list of reports + """ + report_detail = ReportDetailSchema_0_4() + validator = colander.Length(1) + + +class ReportSchemaBase(colander.MappingSchema): + """ + Validates format of report group + """ + client = colander.SchemaNode(colander.String(), + preparer=lambda x: x or 'unknown') + server = colander.SchemaNode( + colander.String(), + preparer=[ + lambda x: x.lower() if x else 'unknown', shortener_factory(128)], + missing='unknown') + priority = colander.SchemaNode(colander.Int(), + preparer=[lambda x: x or 5], + validator=colander.Range(1, 10), + missing=5) + language = colander.SchemaNode(colander.String(), missing='unknown') + error = colander.SchemaNode(colander.String(), + preparer=shortener_factory(512), + missing='') + view_name = colander.SchemaNode(colander.String(), + preparer=[shortener_factory(128), + lambda x: x or ''], + missing='') + http_status = colander.SchemaNode(colander.Int(), + preparer=[lambda x: x or 200], + validator=colander.Range(1)) + + occurences = colander.SchemaNode(colander.Int(), + validator=colander.Range(1, 99999999999), + missing=1) + tags = TagSchemaList() + + +class ReportSchema_0_4(ReportSchemaBase): + error_type = colander.SchemaNode(colander.String(), + preparer=[shortener_factory(512)], + missing='') + report_details = ReportDetailListSchema() + + +class ReportSchema_0_5(ReportSchemaBase, ReportDetailSchema_0_5): + pass + + +class ReportListSchema_0_4(colander.SequenceSchema): + """ + Validates format of list of report groups + """ + report = ReportSchema_0_4() + validator = colander.Length(1) + + +class ReportListSchema_0_5(colander.SequenceSchema): + """ + Validates format of list of report groups + """ + report = ReportSchema_0_5() + validator = colander.Length(1) + + +class LogSchema(colander.MappingSchema): + """ + Validates format if individual log entry + """ + primary_key = colander.SchemaNode(UnknownType(), + preparer=[cast_to_unicode_or_null, + shortener_factory(128)], + missing=None) + log_level = colander.SchemaNode(colander.String(), + preparer=shortener_factory(10), + missing='UNKNOWN') + message = colander.SchemaNode(colander.String(), + preparer=shortener_factory(4096), + missing='') + namespace = colander.SchemaNode(colander.String(), + preparer=shortener_factory(128), + missing='') + request_id = colander.SchemaNode(colander.String(), + preparer=shortener_factory(40), + missing='') + server = colander.SchemaNode(colander.String(), + preparer=shortener_factory(128), + missing='unknown') + date = colander.SchemaNode(NonTZDate(), + validator=limited_date, + missing=deferred_utcnow) + tags = TagSchemaList() + + +class LogSchemaPermanent(LogSchema): + date = colander.SchemaNode(NonTZDate(), + missing=deferred_utcnow) + permanent = colander.SchemaNode(colander.Boolean(), missing=False) + + +class LogListSchema(colander.SequenceSchema): + """ + Validates format of list of log entries + """ + log = LogSchema() + validator = colander.Length(1) + + +class LogListPermanentSchema(colander.SequenceSchema): + """ + Validates format of list of log entries + """ + log = LogSchemaPermanent() + validator = colander.Length(1) + + +class ViewRequestStatsSchema(RequestStatsSchema): + requests = colander.SchemaNode(colander.Integer(), + validator=colander.Range(0), + missing=0) + + +class ViewMetricTupleSchema(colander.TupleSchema): + """ + Validates list of views and their corresponding request stats object ie: + ["dir/module:func",{"custom": 0.0..}] + """ + view_name = colander.SchemaNode(colander.String(), + preparer=[shortener_factory(128), + lambda x: x or 'unknown'], + missing='unknown') + metrics = ViewRequestStatsSchema() + + +class ViewMetricListSchema(colander.SequenceSchema): + """ + Validates view breakdown stats objects list + {metrics key of server/time object} + """ + view_tuple = ViewMetricTupleSchema() + validator = colander.Length(1) + + +class ViewMetricSchema(colander.MappingSchema): + """ + Validates server/timeinterval object, ie: + {server/time object} + + """ + timestamp = colander.SchemaNode(NonTZDate(), + validator=limited_date, + missing=None) + server = colander.SchemaNode(colander.String(), + preparer=[shortener_factory(128), + lambda x: x or 'unknown'], + missing='unknown') + metrics = ViewMetricListSchema() + + +class GeneralMetricSchema(colander.MappingSchema): + """ + Validates universal metric schema + + """ + namespace = colander.SchemaNode(colander.String(), missing='', + preparer=shortener_factory(128)) + + server_name = colander.SchemaNode(colander.String(), + preparer=[shortener_factory(128), + lambda x: x or 'unknown'], + missing='unknown') + timestamp = colander.SchemaNode(NonTZDate(), validator=limited_date, + missing=deferred_utcnow) + tags = TagSchemaList() + + +class GeneralMetricsListSchema(colander.SequenceSchema): + metric = GeneralMetricSchema() + validator = colander.Length(1) + + +class MetricsListSchema(colander.SequenceSchema): + """ + Validates list of metrics objects ie: + [{server/time object}, ] part + + + """ + metric = ViewMetricSchema() + validator = colander.Length(1) + + +class StringToAppList(object): + """ + Returns validated list of application ids from user query and + set of applications user is allowed to look at + transform string to list containing single integer + """ + + def serialize(self, node, appstruct): + if appstruct is null: + return null + return appstruct + + def deserialize(self, node, cstruct): + if cstruct is null: + return null + + apps = set([int(a) for a in node.bindings['resources']]) + + if isinstance(cstruct, str): + cstruct = [cstruct] + + cstruct = [int(a) for a in cstruct] + + valid_apps = list(apps.intersection(set(cstruct))) + if valid_apps: + return valid_apps + return null + + def cstruct_children(self): + return [] + + +@colander.deferred +def possible_applications_validator(node, kw): + possible_apps = [int(a) for a in kw['resources']] + return colander.All(colander.ContainsOnly(possible_apps), + colander.Length(1)) + + +@colander.deferred +def possible_applications(node, kw): + return [int(a) for a in kw['resources']] + + +@colander.deferred +def today_start(node, kw): + return datetime.datetime.utcnow().replace(second=0, microsecond=0, + minute=0, + hour=0) + + +@colander.deferred +def today_end(node, kw): + return datetime.datetime.utcnow().replace(second=0, microsecond=0, + minute=59, hour=23) + + +@colander.deferred +def old_start(node, kw): + t_delta = datetime.timedelta(days=90) + return datetime.datetime.utcnow().replace(second=0, microsecond=0, + minute=0, + hour=0) - t_delta + + +@colander.deferred +def today_end(node, kw): + return datetime.datetime.utcnow().replace(second=0, microsecond=0, + minute=59, hour=23) + + +class PermissiveDate(colander.DateTime): + """ Returns null for incorrect date format - also removes tz info""" + + def deserialize(self, node, cstruct): + if not cstruct: + return null + + try: + result = colander.iso8601.parse_date( + cstruct, default_timezone=self.default_tzinfo) + except colander.iso8601.ParseError: + return null + return result.replace(tzinfo=None) + + +class LogSearchSchema(colander.MappingSchema): + def schema_type(self, **kw): + return colander.Mapping(unknown='preserve') + + resource = colander.SchemaNode(StringToAppList(), + validator=possible_applications_validator, + missing=possible_applications) + + message = colander.SchemaNode(colander.Sequence(accept_scalar=True), + colander.SchemaNode(colander.String()), + missing=None) + level = colander.SchemaNode(colander.Sequence(accept_scalar=True), + colander.SchemaNode(colander.String()), + preparer=lowercase_preparer, + missing=None) + namespace = colander.SchemaNode(colander.Sequence(accept_scalar=True), + colander.SchemaNode(colander.String()), + preparer=lowercase_preparer, + missing=None) + request_id = colander.SchemaNode(colander.Sequence(accept_scalar=True), + colander.SchemaNode(colander.String()), + preparer=lowercase_preparer, + missing=None) + start_date = colander.SchemaNode(PermissiveDate(), + missing=None) + end_date = colander.SchemaNode(PermissiveDate(), + missing=None) + page = colander.SchemaNode(colander.Integer(), + validator=colander.Range(min=1), + missing=1) + + +class ReportSearchSchema(colander.MappingSchema): + def schema_type(self, **kw): + return colander.Mapping(unknown='preserve') + + resource = colander.SchemaNode(StringToAppList(), + validator=possible_applications_validator, + missing=possible_applications) + request_id = colander.SchemaNode(colander.Sequence(accept_scalar=True), + colander.SchemaNode(colander.String()), + missing=None) + start_date = colander.SchemaNode(PermissiveDate(), + missing=None) + end_date = colander.SchemaNode(PermissiveDate(), + missing=None) + page = colander.SchemaNode(colander.Integer(), + validator=colander.Range(min=1), + missing=1) + + min_occurences = colander.SchemaNode( + colander.Sequence(accept_scalar=True), + colander.SchemaNode(colander.Integer()), + missing=None) + + http_status = colander.SchemaNode(colander.Sequence(accept_scalar=True), + colander.SchemaNode(colander.Integer()), + missing=None) + priority = colander.SchemaNode(colander.Sequence(accept_scalar=True), + colander.SchemaNode(colander.Integer()), + missing=None) + error = colander.SchemaNode(colander.Sequence(accept_scalar=True), + colander.SchemaNode(colander.String()), + missing=None) + url_path = colander.SchemaNode(colander.Sequence(accept_scalar=True), + colander.SchemaNode(colander.String()), + missing=None) + url_domain = colander.SchemaNode(colander.Sequence(accept_scalar=True), + colander.SchemaNode(colander.String()), + missing=None) + report_status = colander.SchemaNode(colander.Sequence(accept_scalar=True), + colander.SchemaNode(colander.String()), + missing=None) + min_duration = colander.SchemaNode(colander.Sequence(accept_scalar=True), + colander.SchemaNode(colander.Float()), + missing=None) + max_duration = colander.SchemaNode(colander.Sequence(accept_scalar=True), + colander.SchemaNode(colander.Float()), + missing=None) + + +class TagSchema(colander.MappingSchema): + """ + Used in log search + """ + name = colander.SchemaNode(colander.String(), + validator=colander.Length(1, 32)) + value = colander.SchemaNode(colander.Sequence(accept_scalar=True), + colander.SchemaNode(colander.String(), + validator=colander.Length( + 1, 128)), + missing=None) + op = colander.SchemaNode(colander.String(), + validator=colander.Length(1, 128), + missing=None) + + +class TagListSchema(colander.SequenceSchema): + tag = TagSchema() + + +class RuleFieldType(object): + """ Validator which succeeds if the value passed to it is one of + a fixed set of values """ + + def __init__(self, cast_to): + self.cast_to = cast_to + + def __call__(self, node, value): + try: + if self.cast_to == 'int': + int(value) + elif self.cast_to == 'float': + float(value) + elif self.cast_to == 'unicode': + str(value) + except: + raise colander.Invalid(node, + "Can't cast {} to {}".format( + value, self.cast_to)) + + +def build_rule_schema(ruleset, check_matrix): + """ + Accepts ruleset and a map of fields/possible operations and builds + validation class + """ + + schema = colander.SchemaNode(colander.Mapping()) + schema.add(colander.SchemaNode(colander.String(), name='field')) + + if ruleset['field'] in ['__AND__', '__OR__']: + subrules = colander.SchemaNode(colander.Tuple(), name='rules') + for rule in ruleset['rules']: + subrules.add(build_rule_schema(rule, check_matrix)) + schema.add(subrules) + else: + op_choices = check_matrix[ruleset['field']]['ops'] + cast_to = check_matrix[ruleset['field']]['type'] + schema.add(colander.SchemaNode(colander.String(), + validator=colander.OneOf(op_choices), + name='op')) + + schema.add(colander.SchemaNode(colander.String(), + name='value', + validator=RuleFieldType(cast_to))) + return schema + + +class ConfigTypeSchema(colander.MappingSchema): + type = colander.SchemaNode(colander.String(), missing=None) + config = colander.SchemaNode(UnknownType(), missing=None) + + +class MappingListSchema(colander.SequenceSchema): + config = colander.SchemaNode(UnknownType()) diff --git a/backend/src/appenlight/views/__init__.py b/backend/src/appenlight/views/__init__.py new file mode 100644 index 0000000..5c67c2f --- /dev/null +++ b/backend/src/appenlight/views/__init__.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +"""View handlers package. +""" +from pyramid.response import Response +import logging +import simplejson as json +from appenlight.lib import helpers + +log = logging.getLogger(__name__) + + +def includeme(config): + """Add the application's view handlers. + """ + + config.add_route('/', '/') + config.add_route('angular_app_ui_ix', '/ui') + config.add_route('angular_app_ui', '/ui/*remainder') + + # applications API + config.add_route('applications_no_id', '/applications') + config.add_route('applications', '/applications/{resource_id}', + factory='appenlight.security.ResourceFactory') + config.add_route('applications_property', + '/applications/{resource_id}/{key}', + factory='appenlight.security.ResourceFactory') + config.add_route( + 'integrations_id', + '/applications/{resource_id}/integrations/{integration}/{action}', + factory='appenlight.security.ResourceFactory') + + # users API + config.add_route('users_self', '/users/self') + config.add_route('users_self_property', '/users/self/{key}') + config.add_route('users_no_id', '/users') + config.add_route('users', '/users/{user_id}') + config.add_route('users_property', '/users/{user_id}/{key}') + + # events + config.add_route('events_no_id', '/events') + config.add_route('events', '/events/{event_id}') + config.add_route('events_property', '/events/{event_id}/{key}') + + # groups + config.add_route('groups_no_id', '/groups') + config.add_route('groups', '/groups/{group_id}') + config.add_route('groups_property', '/groups/{group_id}/{key}') + + # reports API + config.add_route('reports', '/reports') + config.add_route('slow_reports', '/slow_reports') + config.add_route('report_groups', '/report_groups/{group_id}', + factory='appenlight.security.ResourceReportFactory') + config.add_route('report_groups_property', + '/report_groups/{group_id}/{key}', + factory='appenlight.security.ResourceReportFactory') + + #generic resource API + config.add_route('resources_property', + '/resources/{resource_id}/{key}', + factory='appenlight.security.ResourceFactory') + + # plugin configs API + config.add_route('plugin_configs', '/plugin_configs/{plugin_name}', + factory='appenlight.security.ResourcePluginMixedFactory') + config.add_route('plugin_config', '/plugin_configs/{plugin_name}/{id}', + factory='appenlight.security.ResourcePluginConfigFactory') + + # client endpoints API + config.add_route('api_reports', '/api/reports', + factory='appenlight.security.APIFactory') + config.add_route('api_report', '/api/report', + factory='appenlight.security.APIFactory') + config.add_route('api_logs', '/api/logs', + factory='appenlight.security.APIFactory') + config.add_route('api_log', '/api/log', + factory='appenlight.security.APIFactory') + config.add_route('api_slow_reports', '/api/slow_reports', + factory='appenlight.security.APIFactory') + config.add_route('api_request_stats', '/api/request_stats', + factory='appenlight.security.APIFactory') + config.add_route('api_metrics', '/api/metrics', + factory='appenlight.security.APIFactory') + config.add_route('api_general_metrics', '/api/general_metrics', + factory='appenlight.security.APIFactory') + config.add_route('api_general_metric', '/api/general_metric', + factory='appenlight.security.APIFactory') + config.add_route('api_airbrake', '/notifier_api/v2/{action}', + factory='appenlight.security.AirbrakeV2APIFactory') + config.add_route('api_sentry', '/api/{project}/store', + factory='appenlight.security.SentryAPIFactory') + config.add_route('api_sentry_slash', '/api/{project}/store/', + factory='appenlight.security.SentryAPIFactory') + + # other + config.add_route('register', '/register') + config.add_route('register_ajax', '/register_ajax') + config.add_route('lost_password', '/lost_password') + config.add_route('lost_password_generate', '/lost_password_generate') + config.add_route('logs_no_id', '/logs') + config.add_route('forbidden', '/forbidden') + config.add_route('test', '/test/{action}') + config.add_route('section_view', '/sections/{section}/{view}') + + config.add_view('appenlight.views.forbidden_view', + context='pyramid.exceptions.Forbidden', + renderer='appenlight:templates/forbidden.jinja2', + permission='__no_permission_required__') + config.add_view('appenlight.views.not_found_view', + context='pyramid.exceptions.NotFound', + renderer='appenlight:templates/not_found.jinja2', + permission='__no_permission_required__') + config.add_view('appenlight.views.csrf_view', + context='appenlight.lib.request.CSRFException', + renderer='appenlight:templates/forbidden.jinja2', + permission='__no_permission_required__') + config.add_view('appenlight.views.csrf_view', + context='appenlight.forms.CSRFException', + renderer='appenlight:templates/forbidden.jinja2', + permission='__no_permission_required__') + config.add_view('appenlight.views.colander_invalid_view', + context='colander.Invalid', + renderer='json', + permission='__no_permission_required__') + config.add_view('appenlight.views.bad_json_view', + context='appenlight.lib.request.JSONException', + renderer='json', + permission='__no_permission_required__') + + # handle authomatic + config.add_route('social_auth', '/social_auth/{provider}') + config.add_route('social_auth_abort', '/social_auth/{provider}/abort') + + # only use in production + if (config.registry.settings.get('pyramid.reload_templates') is False + and config.registry.settings.get('pyramid.debug_templates') is False): + config.add_view('appenlight.views.error_view', + context=Exception, + renderer='appenlight:templates/error.jinja2', + permission='__no_permission_required__') + + +def bad_json_view(exc, request): + request.environ['appenlight.ignore_error'] = 1 + request.response.headers.add('X-AppEnlight-Error', 'Incorrect JSON') + request.response.status_int = 400 + return "Incorrect JSON" + + +def colander_invalid_view(exc, request): + request.environ['appenlight.ignore_error'] = 1 + log.warning('API version %s, %s' % ( + request.params.get('protocol_version'), + request.context.resource)) + log.warning('Invalid payload sent') + errors = exc.asdict() + request.response.headers.add('X-AppEnlight-Error', 'Invalid payload sent') + request.response.status_int = 422 + return errors + + +def csrf_view(exc, request): + request.response.status = 403 + from ..models import DBSession + request.environ["appenlight.ignore_error"] = 1 + request.response.headers.add('X-AppEnlight-Error', str(exc)) + if request.user: + request.user = DBSession.merge(request.user) + return {'forbidden_view': True, 'csrf': True} + + +def not_found_view(exc, request): + request.response.status = 404 + from ..models import DBSession + + if request.user: + request.user = DBSession.merge(request.user) + + if request.user: + request.response.headers['x-appenlight-uid'] = '%s' % request.user.id + request.response.headers['x-appenlight-flash'] = json.dumps( + helpers.get_flash(request)) + + return {} + + +def forbidden_view(exc, request): + # dont serve html for api requests + from ..models import DBSession + + if request.user: + request.user = DBSession.merge(request.user) + if request.path.startswith('/api'): + logging.warning('Wrong API Key sent') + logging.info(request.url) + logging.info( + '\n'.join( + ['%s:%s' % (k, v) for k, v in request.headers.items()])) + resp = Response( + "Wrong api key", + headers=(('X-AppEnlight-Error', 'Incorrect API key',),)) + resp.status_int = 403 + return resp + + if request.user: + request.response.headers['x-appenlight-uid'] = '%s' % request.user.id + request.response.headers['x-appenlight-flash'] = json.dumps( + helpers.get_flash(request)) + request.response.status = 403 + return {'forbidden_view': True} + + +def error_view(exc, request): + from ..models import DBSession + if request.user: + request.user = DBSession.merge(request.user) + if request.path.startswith('/api'): + resp = Response( + "There was a problem handling your request please try again", + headers=(('X-AppEnlight-Error', 'Problem handling request',),) + ) + resp.status_int = 500 + return resp + log.error(exc) + request.response.status = 500 + return {} diff --git a/backend/src/appenlight/views/admin/__init__.py b/backend/src/appenlight/views/admin/__init__.py new file mode 100644 index 0000000..10d0823 --- /dev/null +++ b/backend/src/appenlight/views/admin/__init__.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +"""View handlers package. +""" +import logging + +log = logging.getLogger(__name__) + + +def includeme(config): + """Add the application's view handlers. + """ + config.add_route('admin_users_no_id', '/admin/users/') + config.add_route('admin_users', '/admin/users/{user_id}') + config.add_route('admin', '/admin/{action}') + config.add_route('admin_configs', '/configs') + config.add_route('admin_config', '/configs/{key}/{section}') diff --git a/backend/src/appenlight/views/admin/admin.py b/backend/src/appenlight/views/admin/admin.py new file mode 100644 index 0000000..53d805d --- /dev/null +++ b/backend/src/appenlight/views/admin/admin.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import logging +import os +import pkg_resources + +from datetime import datetime, timedelta + +import psutil +import redis + +from pyramid.view import view_config +from appenlight.models import DBSession +from appenlight.models import Datastores +from appenlight.lib.redis_keys import REDIS_KEYS + + +def bytes2human(total): + giga = 1024.0 ** 3 + mega = 1024.0 ** 2 + kilo = 1024.0 + if giga <= total: + return '{:0.1f}G'.format(total / giga) + elif mega <= total: + return '{:0.1f}M'.format(total / mega) + else: + return '{:0.1f}K'.format(total / kilo) + + +log = logging.getLogger(__name__) + + +@view_config(route_name='section_view', + match_param=['section=admin_section', 'view=system'], + renderer='json', permission='root_administration') +def system(request): + current_time = datetime.utcnow(). \ + replace(second=0, microsecond=0) - timedelta(minutes=1) + # global app counter + + processed_reports = Datastores.redis.get( + REDIS_KEYS['counters']['reports_per_minute'].format(current_time)) + processed_reports = int(processed_reports) if processed_reports else 0 + processed_logs = Datastores.redis.get( + REDIS_KEYS['counters']['logs_per_minute'].format(current_time)) + processed_logs = int(processed_logs) if processed_logs else 0 + processed_metrics = Datastores.redis.get( + REDIS_KEYS['counters']['metrics_per_minute'].format(current_time)) + processed_metrics = int(processed_metrics) if processed_metrics else 0 + + waiting_reports = 0 + waiting_logs = 0 + waiting_metrics = 0 + waiting_other = 0 + + if 'redis' in request.registry.settings['celery.broker_type']: + redis_client = redis.StrictRedis.from_url( + request.registry.settings['celery.broker_url']) + waiting_reports = redis_client.llen('reports') + waiting_logs = redis_client.llen('logs') + waiting_metrics = redis_client.llen('metrics') + waiting_other = redis_client.llen('default') + + # process + def replace_inf(val): + return val if val != psutil.RLIM_INFINITY else 'unlimited' + + p = psutil.Process() + fd = p.rlimit(psutil.RLIMIT_NOFILE) + memlock = p.rlimit(psutil.RLIMIT_MEMLOCK) + self_info = { + 'fds': {'soft': replace_inf(fd[0]), + 'hard': replace_inf(fd[1])}, + 'memlock': {'soft': replace_inf(memlock[0]), + 'hard': replace_inf(memlock[1])}, + } + + # disks + disks = [] + for part in psutil.disk_partitions(all=False): + if os.name == 'nt': + if 'cdrom' in part.opts or part.fstype == '': + continue + usage = psutil.disk_usage(part.mountpoint) + disks.append({ + 'device': part.device, + 'total': bytes2human(usage.total), + 'used': bytes2human(usage.used), + 'free': bytes2human(usage.free), + 'percentage': int(usage.percent), + 'mountpoint': part.mountpoint, + 'fstype': part.fstype + }) + + # memory + memory_v = psutil.virtual_memory() + memory_s = psutil.swap_memory() + + memory = { + 'total': bytes2human(memory_v.total), + 'available': bytes2human(memory_v.available), + 'percentage': memory_v.percent, + 'used': bytes2human(memory_v.used), + 'free': bytes2human(memory_v.free), + 'active': bytes2human(memory_v.active), + 'inactive': bytes2human(memory_v.inactive), + 'buffers': bytes2human(memory_v.buffers), + 'cached': bytes2human(memory_v.cached), + 'swap_total': bytes2human(memory_s.total), + 'swap_used': bytes2human(memory_s.used) + } + + # load + system_load = os.getloadavg() + + # processes + min_mem = 1024 * 1024 * 40 # 40MB + process_info = [] + for p in psutil.process_iter(): + mem_used = p.get_memory_info().rss + if mem_used < min_mem: + continue + process_info.append({'owner': p.username(), + 'pid': p.pid, + 'cpu': round(p.get_cpu_percent(interval=0), 1), + 'mem_percentage': round(p.get_memory_percent(), + 1), + 'mem_usage': bytes2human(mem_used), + 'name': p.name(), + 'command': ' '.join(p.cmdline()) + }) + process_info = sorted(process_info, key=lambda x: x['mem_percentage'], + reverse=True) + + # pg tables + + db_size_query = ''' + SELECT tablename, pg_total_relation_size(tablename::text) size + FROM pg_tables WHERE tablename NOT LIKE 'pg_%' AND + tablename NOT LIKE 'sql_%' ORDER BY size DESC;''' + + db_tables = [] + for row in DBSession.execute(db_size_query): + db_tables.append({"size_human": bytes2human(row.size), + "table_name": row.tablename}) + + # es indices + es_indices = [] + result = Datastores.es.send_request( + 'GET', ['_stats', 'store, docs'], query_params={}) + for ix, stats in result['indices'].items(): + size = stats['primaries']['store']['size_in_bytes'] + es_indices.append({'name': ix, + 'size': size, + 'size_human': bytes2human(size)}) + + # packages + + packages = ({'name': p.project_name, 'version': p.version} + for p in pkg_resources.working_set) + + return {'db_tables': db_tables, + 'es_indices': sorted(es_indices, + key=lambda x: x['size'], reverse=True), + 'process_info': process_info, + 'system_load': system_load, + 'disks': disks, + 'memory': memory, + 'packages': sorted(packages, key=lambda x: x['name'].lower()), + 'current_time': current_time, + 'queue_stats': { + 'processed_reports': processed_reports, + 'processed_logs': processed_logs, + 'processed_metrics': processed_metrics, + 'waiting_reports': waiting_reports, + 'waiting_logs': waiting_logs, + 'waiting_metrics': waiting_metrics, + 'waiting_other': waiting_other + }, + 'self_info': self_info + } diff --git a/backend/src/appenlight/views/admin/config.py b/backend/src/appenlight/views/admin/config.py new file mode 100644 index 0000000..d5c7abe --- /dev/null +++ b/backend/src/appenlight/views/admin/config.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +from pyramid.view import view_config +from pyramid.httpexceptions import HTTPNotFound +from appenlight.models.services.config import ConfigService + +import logging + +log = logging.getLogger(__name__) + + +@view_config(route_name='admin_configs', renderer='json', + permission='root_administration', request_method='GET') +def query(request): + ConfigService.setup_default_values() + pairs = [] + for value in request.GET.getall('filter'): + split = value.split(':', 1) + pairs.append({'key': split[0], 'section': split[1]}) + return [c for c in ConfigService.filtered_key_and_section(pairs)] + + +@view_config(route_name='admin_config', renderer='json', + permission='root_administration', request_method='POST') +def post(request): + row = ConfigService.by_key_and_section( + key=request.matchdict.get('key'), + section=request.matchdict.get('section')) + if not row: + raise HTTPNotFound() + row.value = None + row.value = request.unsafe_json_body['value'] + return row diff --git a/backend/src/appenlight/views/admin/partitions.py b/backend/src/appenlight/views/admin/partitions.py new file mode 100644 index 0000000..54786ce --- /dev/null +++ b/backend/src/appenlight/views/admin/partitions.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +from pyramid.view import view_config +from appenlight.models import DBSession, Datastores +from appenlight.forms import get_partition_deletion_form + +import logging + +from zope.sqlalchemy import mark_changed +from datetime import datetime +import sqlalchemy as sa + +log = logging.getLogger(__name__) + + +def get_partition_stats(): + table_query = """ + SELECT table_name + FROM information_schema.tables + GROUP BY table_name + ORDER BY table_name + """ + + permanent_partitions = {} + daily_partitions = {} + + def is_int(data): + try: + int(data) + return True + except Exception: + pass + return False + + def add_key(key, holder): + if not ix_time in holder: + holder[ix_time] = {'pg': [], 'elasticsearch': []} + + for partition in list(Datastores.es.aliases().keys()): + if not partition.startswith('rcae'): + continue + split_data = partition.split('_') + permanent = False + # if we dont have a day then treat it as permanent partion + if False in list(map(is_int, split_data[-3:])): + ix_time = datetime(year=int(split_data[-2]), + month=int(split_data[-1]), + day=1).date() + permanent = True + else: + ix_time = datetime(year=int(split_data[-3]), + month=int(split_data[-2]), + day=int(split_data[-1])).date() + + ix_time = str(ix_time) + if permanent: + add_key(ix_time, permanent_partitions) + if ix_time not in permanent_partitions: + permanent_partitions[ix_time]['elasticsearch'] = [] + permanent_partitions[ix_time]['elasticsearch'].append(partition) + else: + add_key(ix_time, daily_partitions) + if ix_time not in daily_partitions: + daily_partitions[ix_time]['elasticsearch'] = [] + daily_partitions[ix_time]['elasticsearch'].append(partition) + + for row in DBSession.execute(table_query): + splitted = row['table_name'].split('_') + if 'p' in splitted: + # dealing with partition + split_data = [int(x) for x in splitted[splitted.index('p') + 1:]] + if len(split_data) == 3: + ix_time = datetime(split_data[0], split_data[1], + split_data[2]).date() + ix_time = str(ix_time) + add_key(ix_time, daily_partitions) + daily_partitions[ix_time]['pg'].append(row['table_name']) + else: + ix_time = datetime(split_data[0], split_data[1], 1).date() + ix_time = str(ix_time) + add_key(ix_time, permanent_partitions) + permanent_partitions[ix_time]['pg'].append(row['table_name']) + + return permanent_partitions, daily_partitions + + +@view_config(route_name='section_view', permission='root_administration', + match_param=['section=admin_section', 'view=partitions'], + renderer='json', request_method='GET') +def index(request): + permanent_partitions, daily_partitions = get_partition_stats() + + return {"permanent_partitions": sorted(list(permanent_partitions.items()), + key=lambda x: x[0], reverse=True), + "daily_partitions": sorted(list(daily_partitions.items()), + key=lambda x: x[0], reverse=True)} + + +@view_config(route_name='section_view', request_method='POST', + match_param=['section=admin_section', 'view=partitions_remove'], + renderer='json', permission='root_administration') +def partitions_remove(request): + permanent_partitions, daily_partitions = get_partition_stats() + pg_partitions = [] + es_partitions = [] + for item in list(permanent_partitions.values()) + list(daily_partitions.values()): + es_partitions.extend(item['elasticsearch']) + pg_partitions.extend(item['pg']) + FormCls = get_partition_deletion_form(es_partitions, pg_partitions) + form = FormCls(es_index=request.unsafe_json_body['es_indices'], + pg_index=request.unsafe_json_body['pg_indices'], + confirm=request.unsafe_json_body['confirm'], + csrf_context=request) + if form.validate(): + for ix in form.data['es_index']: + Datastores.es.delete_index(ix) + for ix in form.data['pg_index']: + stmt = sa.text('DROP TABLE %s CASCADE' % sa.text(ix)) + session = DBSession() + session.connection().execute(stmt) + mark_changed(session) + + for field, error in form.errors.items(): + msg = '%s: %s' % (field, error[0]) + request.session.flash(msg, 'error') + + permanent_partitions, daily_partitions = get_partition_stats() + return { + "permanent_partitions": sorted( + list(permanent_partitions.items()), key=lambda x: x[0], reverse=True), + "daily_partitions": sorted( + list(daily_partitions.items()), key=lambda x: x[0], reverse=True)} diff --git a/backend/src/appenlight/views/admin/users.py b/backend/src/appenlight/views/admin/users.py new file mode 100644 index 0000000..935320a --- /dev/null +++ b/backend/src/appenlight/views/admin/users.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +from pyramid.view import view_config +from pyramid.httpexceptions import HTTPFound, HTTPNotFound +from pyramid import security +from appenlight.models.user import User + +import logging + +log = logging.getLogger(__name__) + + +@view_config(route_name='section_view', permission='root_administration', + match_param=['section=admin_section', 'view=relogin_user'], + renderer='json', request_method='GET') +def relogin_to_user(request): + user = User.by_id(request.GET.get('user_id')) + if not user: + return HTTPNotFound() + headers = security.remember(request, user.id) + return HTTPFound(location=request.route_url('/'), + headers=headers) diff --git a/backend/src/appenlight/views/api.py b/backend/src/appenlight/views/api.py new file mode 100644 index 0000000..817bad7 --- /dev/null +++ b/backend/src/appenlight/views/api.py @@ -0,0 +1,422 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import base64 +import io +import datetime +import json +import logging +import urllib.request, urllib.parse, urllib.error +import zlib + +from gzip import GzipFile +from pyramid.view import view_config +from pyramid.httpexceptions import HTTPBadRequest + +import appenlight.celery.tasks as tasks +from appenlight.lib.api import rate_limiting, check_cors +from appenlight.lib.enums import ParsedSentryEventType +from appenlight.lib.utils import parse_proto +from appenlight.lib.utils.airbrake import parse_airbrake_xml +from appenlight.lib.utils.date_utils import convert_date +from appenlight.lib.utils.sentry import parse_sentry_event +from appenlight.lib.request import JSONException +from appenlight.validators import (LogListSchema, + MetricsListSchema, + GeneralMetricsListSchema, + GeneralMetricSchema, + LogListPermanentSchema, + ReportListSchema_0_5, + LogSchema, + LogSchemaPermanent, + ReportSchema_0_5) + +log = logging.getLogger(__name__) + + +@view_config(route_name='api_logs', renderer='string', permission='create', + require_csrf=False) +@view_config(route_name='api_log', renderer='string', permission='create', + require_csrf=False) +def logs_create(request): + """ + Endpoint for log aggregation + """ + application = request.context.resource + if request.method.upper() == 'OPTIONS': + return check_cors(request, application) + else: + check_cors(request, application, should_return=False) + + params = dict(request.params.copy()) + proto_version = parse_proto(params.get('protocol_version', '')) + payload = request.unsafe_json_body + sequence_accepted = request.matched_route.name == 'api_logs' + + if sequence_accepted: + if application.allow_permanent_storage: + schema = LogListPermanentSchema().bind( + utcnow=datetime.datetime.utcnow()) + else: + schema = LogListSchema().bind( + utcnow=datetime.datetime.utcnow()) + else: + if application.allow_permanent_storage: + schema = LogSchemaPermanent().bind( + utcnow=datetime.datetime.utcnow()) + else: + schema = LogSchema().bind( + utcnow=datetime.datetime.utcnow()) + + deserialized_logs = schema.deserialize(payload) + if sequence_accepted is False: + deserialized_logs = [deserialized_logs] + + rate_limiting(request, application, 'per_application_logs_rate_limit', + len(deserialized_logs)) + + # pprint.pprint(deserialized_logs) + + # we need to split those out so we can process the pkey ones one by one + non_pkey_logs = [log_dict for log_dict in deserialized_logs + if not log_dict['primary_key']] + pkey_dict = {} + # try to process the logs as best as we can and group together to reduce + # the amount of + for log_dict in deserialized_logs: + if log_dict['primary_key']: + key = (log_dict['primary_key'], log_dict['namespace'],) + if not key in pkey_dict: + pkey_dict[key] = [] + pkey_dict[key].append(log_dict) + + if non_pkey_logs: + log.debug('%s non-pkey logs received: %s' % (application, + len(non_pkey_logs))) + tasks.add_logs.delay(application.resource_id, params, non_pkey_logs) + if pkey_dict: + logs_to_insert = [] + for primary_key_tuple, payload in pkey_dict.items(): + sorted_logs = sorted(payload, key=lambda x: x['date']) + logs_to_insert.append(sorted_logs[-1]) + log.debug('%s pkey logs received: %s' % (application, + len(logs_to_insert))) + tasks.add_logs.delay(application.resource_id, params, logs_to_insert) + + log.info('LOG call %s %s client:%s' % ( + application, proto_version, request.headers.get('user_agent'))) + return 'OK: Logs accepted' + + +@view_config(route_name='api_request_stats', renderer='string', + permission='create', require_csrf=False) +@view_config(route_name='api_metrics', renderer='string', + permission='create', require_csrf=False) +def request_metrics_create(request): + """ + Endpoint for performance metrics, aggregates view performance stats + and converts them to general metric row + """ + application = request.context.resource + if request.method.upper() == 'OPTIONS': + return check_cors(request, application) + else: + check_cors(request, application, should_return=False) + + params = dict(request.params.copy()) + proto_version = parse_proto(params.get('protocol_version', '')) + + payload = request.unsafe_json_body + schema = MetricsListSchema() + dataset = schema.deserialize(payload) + + rate_limiting(request, application, 'per_application_metrics_rate_limit', + len(dataset)) + + # looping report data + metrics = {} + for metric in dataset: + server_name = metric.get('server', '').lower() or 'unknown' + start_interval = convert_date(metric['timestamp']) + start_interval = start_interval.replace(second=0, microsecond=0) + + for view_name, view_metrics in metric['metrics']: + key = '%s%s%s' % (metric['server'], start_interval, view_name) + if start_interval not in metrics: + metrics[key] = {"requests": 0, "main": 0, "sql": 0, + "nosql": 0, "remote": 0, "tmpl": 0, + "custom": 0, 'sql_calls': 0, + 'nosql_calls': 0, + 'remote_calls': 0, 'tmpl_calls': 0, + 'custom_calls': 0, + "start_interval": start_interval, + "server_name": server_name, + "view_name": view_name + } + metrics[key]["requests"] += int(view_metrics['requests']) + metrics[key]["main"] += round(view_metrics['main'], 5) + metrics[key]["sql"] += round(view_metrics['sql'], 5) + metrics[key]["nosql"] += round(view_metrics['nosql'], 5) + metrics[key]["remote"] += round(view_metrics['remote'], 5) + metrics[key]["tmpl"] += round(view_metrics['tmpl'], 5) + metrics[key]["custom"] += round(view_metrics.get('custom', 0.0), + 5) + metrics[key]["sql_calls"] += int( + view_metrics.get('sql_calls', 0)) + metrics[key]["nosql_calls"] += int( + view_metrics.get('nosql_calls', 0)) + metrics[key]["remote_calls"] += int( + view_metrics.get('remote_calls', 0)) + metrics[key]["tmpl_calls"] += int( + view_metrics.get('tmpl_calls', 0)) + metrics[key]["custom_calls"] += int( + view_metrics.get('custom_calls', 0)) + + if not metrics[key]["requests"]: + # fix this here because validator can't + metrics[key]["requests"] = 1 + # metrics dict is being built to minimize + # the amount of queries used + # in case we get multiple rows from same minute + + normalized_metrics = [] + for metric in metrics.values(): + new_metric = { + 'namespace': 'appenlight.request_metric', + 'timestamp': metric.pop('start_interval'), + 'server_name': metric['server_name'], + 'tags': list(metric.items()) + } + normalized_metrics.append(new_metric) + + tasks.add_metrics.delay(application.resource_id, params, + normalized_metrics, proto_version) + + log.info('REQUEST METRICS call {} {} client:{}'.format( + application.resource_name, proto_version, + request.headers.get('user_agent'))) + return 'OK: request metrics accepted' + + +@view_config(route_name='api_general_metrics', renderer='string', + permission='create', require_csrf=False) +@view_config(route_name='api_general_metric', renderer='string', + permission='create', require_csrf=False) +def general_metrics_create(request): + """ + Endpoint for general metrics aggregation + """ + application = request.context.resource + if request.method.upper() == 'OPTIONS': + return check_cors(request, application) + else: + check_cors(request, application, should_return=False) + + params = dict(request.params.copy()) + proto_version = parse_proto(params.get('protocol_version', '')) + payload = request.unsafe_json_body + sequence_accepted = request.matched_route.name == 'api_general_metrics' + if sequence_accepted: + schema = GeneralMetricsListSchema().bind( + utcnow=datetime.datetime.utcnow()) + else: + schema = GeneralMetricSchema().bind(utcnow=datetime.datetime.utcnow()) + + deserialized_metrics = schema.deserialize(payload) + if sequence_accepted is False: + deserialized_metrics = [deserialized_metrics] + + rate_limiting(request, application, 'per_application_metrics_rate_limit', + len(deserialized_metrics)) + + tasks.add_metrics.delay(application.resource_id, params, + deserialized_metrics, proto_version) + + log.info('METRICS call {} {} client:{}'.format( + application.resource_name, proto_version, + request.headers.get('user_agent'))) + return 'OK: Metrics accepted' + + +@view_config(route_name='api_reports', renderer='string', permission='create', + require_csrf=False) +@view_config(route_name='api_slow_reports', renderer='string', + permission='create', require_csrf=False) +@view_config(route_name='api_report', renderer='string', permission='create', + require_csrf=False) +def reports_create(request): + """ + Endpoint for exception and slowness reports + """ + # route_url('reports') + application = request.context.resource + if request.method.upper() == 'OPTIONS': + return check_cors(request, application) + else: + check_cors(request, application, should_return=False) + params = dict(request.params.copy()) + proto_version = parse_proto(params.get('protocol_version', '')) + payload = request.unsafe_json_body + sequence_accepted = request.matched_route.name == 'api_reports' + + if sequence_accepted: + schema = ReportListSchema_0_5().bind( + utcnow=datetime.datetime.utcnow()) + else: + schema = ReportSchema_0_5().bind( + utcnow=datetime.datetime.utcnow()) + + deserialized_reports = schema.deserialize(payload) + if sequence_accepted is False: + deserialized_reports = [deserialized_reports] + if deserialized_reports: + rate_limiting(request, application, + 'per_application_reports_rate_limit', + len(deserialized_reports)) + + # pprint.pprint(deserialized_reports) + tasks.add_reports.delay(application.resource_id, params, + deserialized_reports) + log.info('REPORT call %s, %s client:%s' % ( + application, + proto_version, + request.headers.get('user_agent')) + ) + return 'OK: Reports accepted' + + +@view_config(route_name='api_airbrake', renderer='string', permission='create', + require_csrf=False) +def airbrake_xml_compat(request): + """ + Airbrake compatible endpoint for XML reports + """ + application = request.context.resource + if request.method.upper() == 'OPTIONS': + return check_cors(request, application) + else: + check_cors(request, application, should_return=False) + + params = request.params.copy() + + error_dict = parse_airbrake_xml(request) + schema = ReportListSchema_0_5().bind(utcnow=datetime.datetime.utcnow()) + deserialized_reports = schema.deserialize([error_dict]) + rate_limiting(request, application, 'per_application_reports_rate_limit', + len(deserialized_reports)) + + tasks.add_reports.delay(application.resource_id, params, + deserialized_reports) + log.info('%s AIRBRAKE call for application %s, api_ver:%s client:%s' % ( + 500, application.resource_name, + request.params.get('protocol_version', 'unknown'), + request.headers.get('user_agent')) + ) + return 'no-id%s' % \ + request.registry.settings['mailing.app_url'] + + +def decompress_gzip(data): + try: + fp = io.StringIO(data) + with GzipFile(fileobj=fp) as f: + return f.read() + except Exception as exc: + raise + log.error(exc) + raise HTTPBadRequest() + + +def decompress_zlib(data): + try: + return zlib.decompress(data) + except Exception as exc: + raise + log.error(exc) + raise HTTPBadRequest() + + +def decode_b64(data): + try: + return base64.b64decode(data) + except Exception as exc: + raise + log.error(exc) + raise HTTPBadRequest() + + +@view_config(route_name='api_sentry', renderer='string', permission='create', + require_csrf=False) +@view_config(route_name='api_sentry_slash', renderer='string', + permission='create', require_csrf=False) +def sentry_compat(request): + """ + Sentry compatible endpoint + """ + application = request.context.resource + if request.method.upper() == 'OPTIONS': + return check_cors(request, application) + else: + check_cors(request, application, should_return=False) + + # handle various report encoding + content_encoding = request.headers.get('Content-Encoding') + content_type = request.headers.get('Content-Type') + if content_encoding == 'gzip': + body = decompress_gzip(request.body) + elif content_encoding == 'deflate': + body = decompress_zlib(request.body) + else: + body = request.body + # attempt to fix string before decoding for stupid clients + if content_type == 'application/x-www-form-urlencoded': + body = urllib.parse.unquote(body.decode('utf8')) + check_char = '{' if isinstance(body, str) else b'{' + if not body.startswith(check_char): + try: + body = decode_b64(body) + body = decompress_zlib(body) + except Exception as exc: + log.info(exc) + + try: + json_body = json.loads(body.decode('utf8')) + except ValueError: + raise JSONException("Incorrect JSON") + + event, event_type = parse_sentry_event(json_body) + + if event_type == ParsedSentryEventType.LOG: + schema = LogSchema().bind(utcnow=datetime.datetime.utcnow()) + deserialized_logs = schema.deserialize(event) + non_pkey_logs = [deserialized_logs] + log.debug('%s non-pkey logs received: %s' % (application, + len(non_pkey_logs))) + tasks.add_logs.delay(application.resource_id, {}, non_pkey_logs) + if event_type == ParsedSentryEventType.ERROR_REPORT: + schema = ReportSchema_0_5().bind(utcnow=datetime.datetime.utcnow()) + deserialized_reports = [schema.deserialize(event)] + rate_limiting(request, application, + 'per_application_reports_rate_limit', + len(deserialized_reports)) + tasks.add_reports.delay(application.resource_id, {}, + deserialized_reports) + return 'OK: Events accepted' diff --git a/backend/src/appenlight/views/applications.py b/backend/src/appenlight/views/applications.py new file mode 100644 index 0000000..35c3e5c --- /dev/null +++ b/backend/src/appenlight/views/applications.py @@ -0,0 +1,760 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import copy +import json +import logging +import six + +from datetime import datetime, timedelta + +import colander +from pyramid.httpexceptions import HTTPFound, HTTPUnprocessableEntity +from pyramid.view import view_config +from webob.multidict import MultiDict +from zope.sqlalchemy import mark_changed +from ziggurat_foundations.permissions import ANY_PERMISSION + +import appenlight.forms as forms +from appenlight.models import DBSession +from appenlight.models.resource import Resource +from appenlight.models.application import Application +from appenlight.models.application_postprocess_conf import \ + ApplicationPostprocessConf +from appenlight.models.user import User +from appenlight.models.user_resource_permission import UserResourcePermission +from appenlight.models.group_resource_permission import GroupResourcePermission +from appenlight.models.services.application import ApplicationService +from appenlight.models.services.application_postprocess_conf import \ + ApplicationPostprocessConfService +from appenlight.models.services.group import GroupService +from appenlight.models.services.group_resource_permission import \ + GroupResourcePermissionService +from appenlight.models.services.request_metric import RequestMetricService +from appenlight.models.services.report_group import ReportGroupService +from appenlight.models.services.slow_call import SlowCallService +from appenlight.lib import helpers as h +from appenlight.lib.utils import build_filter_settings_from_query_dict +from appenlight.security import RootFactory +from appenlight.models.report import REPORT_TYPE_MATRIX +from appenlight.validators import build_rule_schema + +_ = str + +log = logging.getLogger(__name__) + + +def app_not_found(request, id): + """ + Redirects on non found and sets a flash message + """ + request.session.flash(_('Application not found'), 'warning') + return HTTPFound( + location=request.route_url('applications', action='index')) + + +@view_config(route_name='applications_no_id', + renderer='json', request_method="GET", permission='authenticated') +def applications_list(request): + """ + Applications list + + if query params contain ?type=foo, it will list applications + with one of those permissions for user, + otherwise only list of owned applications will + be returned + + appending ?root_list while being administration will allow to list all + applications in the system + + """ + is_root = request.has_permission('root_administration', + RootFactory(request)) + if is_root and request.GET.get('root_list'): + resources = Resource.all().order_by(Resource.resource_name) + resource_type = request.GET.get('resource_type', 'application') + if resource_type: + resources = resources.filter( + Resource.resource_type == resource_type) + else: + permissions = request.params.getall('permission') + if permissions: + resources = request.user.resources_with_perms( + permissions, + resource_types=[request.GET.get('resource_type', + 'application')]) + else: + resources = request.user.resources.filter( + Application.resource_type == request.GET.get( + 'resource_type', + 'application')) + return [r.get_dict(include_keys=['resource_id', 'resource_name', 'domains', + 'owner_user_name', 'owner_group_name']) + for + r in resources] + + +@view_config(route_name='applications', renderer='json', + request_method="GET", permission='view') +def application_GET(request): + resource = request.context.resource + include_sensitive_info = False + if request.has_permission('edit'): + include_sensitive_info = True + resource_dict = resource.get_dict( + include_perms=include_sensitive_info, + include_processing_rules=include_sensitive_info) + return resource_dict + + +@view_config(route_name='applications_no_id', request_method="POST", + renderer='json', permission='create_resources') +def application_create(request): + """ + Creates new application instances + """ + user = request.user + form = forms.ApplicationCreateForm(MultiDict(request.unsafe_json_body), + csrf_context=request) + if form.validate(): + session = DBSession() + resource = Application() + DBSession.add(resource) + form.populate_obj(resource) + resource.api_key = resource.generate_api_key() + user.resources.append(resource) + request.session.flash(_('Application created')) + DBSession.flush() + mark_changed(session) + else: + return HTTPUnprocessableEntity(body=form.errors_json) + + return resource.get_dict() + + +@view_config(route_name='applications', request_method="PATCH", + renderer='json', permission='edit') +def application_update(request): + """ + Updates main application configuration + """ + resource = request.context.resource + if not resource: + return app_not_found() + + # disallow setting permanent storage by non-admins + # use default/non-resource based context for this check + req_dict = copy.copy(request.unsafe_json_body) + if not request.has_permission('root_administration', RootFactory(request)): + req_dict['allow_permanent_storage'] = '' + if not req_dict.get('uptime_url'): + # needed cause validator is still triggered by default + req_dict.pop('uptime_url', '') + application_form = forms.ApplicationUpdateForm(MultiDict(req_dict), + csrf_context=request) + if application_form.validate(): + application_form.populate_obj(resource) + request.session.flash(_('Application updated')) + else: + return HTTPUnprocessableEntity(body=application_form.errors_json) + + include_sensitive_info = False + if request.has_permission('edit'): + include_sensitive_info = True + resource_dict = resource.get_dict( + include_perms=include_sensitive_info, + include_processing_rules=include_sensitive_info) + return resource_dict + + +@view_config(route_name='applications_property', match_param='key=api_key', + request_method="POST", renderer='json', + permission='delete') +def application_regenerate_key(request): + """ + Regenerates API keys for application + """ + resource = request.context.resource + + form = forms.CheckPasswordForm(MultiDict(request.unsafe_json_body), + csrf_context=request) + form.password.user = request.user + + if form.validate(): + resource.api_key = resource.generate_api_key() + resource.public_key = resource.generate_api_key() + msg = 'API keys regenerated - please update your application config.' + request.session.flash(_(msg)) + else: + return HTTPUnprocessableEntity(body=form.errors_json) + + if request.has_permission('edit'): + include_sensitive_info = True + resource_dict = resource.get_dict( + include_perms=include_sensitive_info, + include_processing_rules=include_sensitive_info) + return resource_dict + + +@view_config(route_name='applications_property', + match_param='key=delete_resource', + request_method="PATCH", renderer='json', permission='delete') +def application_remove(request): + """ + Removes application resources + """ + resource = request.context.resource + # we need polymorphic object here, to properly launch sqlalchemy events + resource = ApplicationService.by_id(resource.resource_id) + form = forms.CheckPasswordForm(MultiDict(request.safe_json_body or {}), + csrf_context=request) + form.password.user = request.user + if form.validate(): + DBSession.delete(resource) + request.session.flash(_('Application removed')) + else: + return HTTPUnprocessableEntity(body=form.errors_json) + + return True + + +@view_config(route_name='applications_property', match_param='key=owner', + request_method="PATCH", renderer='json', permission='delete') +def application_ownership_transfer(request): + """ + Allows application owner to transfer application ownership to other user + """ + resource = request.context.resource + form = forms.ChangeApplicationOwnerForm( + MultiDict(request.safe_json_body or {}), csrf_context=request) + form.password.user = request.user + if form.validate(): + user = User.by_user_name(form.user_name.data) + user.resources.append(resource) + # remove integrations to not leak security data of external applications + for integration in resource.integrations[:]: + resource.integrations.remove(integration) + request.session.flash(_('Application transfered')) + else: + return HTTPUnprocessableEntity(body=form.errors_json) + return True + + +@view_config(route_name='applications_property', + match_param='key=postprocessing_rules', renderer='json', + request_method='POST', permission='edit') +def applications_postprocess_POST(request): + """ + Creates new postprocessing rules for applications + """ + resource = request.context.resource + conf = ApplicationPostprocessConf() + conf.do = 'postprocess' + conf.new_value = '1' + resource.postprocess_conf.append(conf) + DBSession.flush() + return conf.get_dict() + + +@view_config(route_name='applications_property', + match_param='key=postprocessing_rules', renderer='json', + request_method='PATCH', permission='edit') +def applications_postprocess_PATCH(request): + """ + Creates new postprocessing rules for applications + """ + json_body = request.unsafe_json_body + + schema = build_rule_schema(json_body['rule'], REPORT_TYPE_MATRIX) + try: + schema.deserialize(json_body['rule']) + except colander.Invalid as exc: + return HTTPUnprocessableEntity(body=json.dumps(exc.asdict())) + + resource = request.context.resource + conf = ApplicationPostprocessConfService.by_pkey_and_resource_id( + json_body['pkey'], resource.resource_id) + conf.rule = request.unsafe_json_body['rule'] + # for now hardcode int since we dont support anything else so far + conf.new_value = int(request.unsafe_json_body['new_value']) + return conf.get_dict() + + +@view_config(route_name='applications_property', + match_param='key=postprocessing_rules', renderer='json', + request_method='DELETE', permission='edit') +def applications_postprocess_DELETE(request): + """ + Removes application postprocessing rules + """ + form = forms.ReactorForm(request.POST, csrf_context=request) + resource = request.context.resource + if form.validate(): + for postprocess_conf in resource.postprocess_conf: + if postprocess_conf.pkey == int(request.GET['pkey']): + # remove rule + DBSession.delete(postprocess_conf) + return True + + +@view_config(route_name='applications_property', + match_param='key=report_graphs', renderer='json', + permission='view') +@view_config(route_name='applications_property', + match_param='key=slow_report_graphs', renderer='json', + permission='view') +def get_application_report_stats(request): + query_params = request.GET.mixed() + query_params['resource'] = (request.context.resource.resource_id,) + + filter_settings = build_filter_settings_from_query_dict(request, + query_params) + if not filter_settings.get('end_date'): + end_date = datetime.utcnow().replace(microsecond=0, second=0) + filter_settings['end_date'] = end_date + + if not filter_settings.get('start_date'): + delta = timedelta(hours=1) + filter_settings['start_date'] = filter_settings['end_date'] - delta + + result = ReportGroupService.get_report_stats(request, filter_settings) + return result + + +@view_config(route_name='applications_property', + match_param='key=metrics_graphs', renderer='json', + permission='view') +def metrics_graphs(request): + """ + Handles metric dashboard graphs + Returns information for time/tier breakdown + """ + query_params = request.GET.mixed() + query_params['resource'] = (request.context.resource.resource_id,) + + filter_settings = build_filter_settings_from_query_dict(request, + query_params) + + if not filter_settings.get('end_date'): + end_date = datetime.utcnow().replace(microsecond=0, second=0) + filter_settings['end_date'] = end_date + + delta = timedelta(hours=1) + if not filter_settings.get('start_date'): + filter_settings['start_date'] = filter_settings['end_date'] - delta + if filter_settings['end_date'] <= filter_settings['start_date']: + filter_settings['end_date'] = filter_settings['start_date'] + + delta = filter_settings['end_date'] - filter_settings['start_date'] + if delta < h.time_deltas.get('12h')['delta']: + divide_by_min = 1 + elif delta <= h.time_deltas.get('3d')['delta']: + divide_by_min = 5.0 + elif delta >= h.time_deltas.get('2w')['delta']: + divide_by_min = 60.0 * 24 + else: + divide_by_min = 60.0 + + results = RequestMetricService.get_metrics_stats( + request, filter_settings) + # because requests are PER SECOND / we divide 1 min stats by 60 + # requests are normalized to 1 min average + # results are average seconds time spent per request in specific area + for point in results: + if point['requests']: + point['main'] = (point['main'] - point['sql'] - + point['nosql'] - point['remote'] - + point['tmpl'] - + point['custom']) / point['requests'] + point['sql'] = point['sql'] / point['requests'] + point['nosql'] = point['nosql'] / point['requests'] + point['remote'] = point['remote'] / point['requests'] + point['tmpl'] = point['tmpl'] / point['requests'] + point['custom'] = point['custom'] / point['requests'] + point['requests_2'] = point['requests'] / 60.0 / divide_by_min + + selected_types = ['main', 'sql', 'nosql', 'remote', 'tmpl', 'custom'] + + for point in results: + for stat_type in selected_types: + point[stat_type] = round(point.get(stat_type, 0), 3) + + return results + + +@view_config(route_name='applications_property', + match_param='key=response_graphs', renderer='json', + permission='view') +def response_graphs(request): + """ + Handles dashboard infomation for avg. response time split by today, + 2 days ago and week ago + """ + query_params = request.GET.mixed() + query_params['resource'] = (request.context.resource.resource_id,) + + filter_settings = build_filter_settings_from_query_dict(request, + query_params) + + if not filter_settings.get('end_date'): + end_date = datetime.utcnow().replace(microsecond=0, second=0) + filter_settings['end_date'] = end_date + + delta = timedelta(hours=1) + if not filter_settings.get('start_date'): + filter_settings['start_date'] = filter_settings['end_date'] - delta + + result_now = RequestMetricService.get_metrics_stats( + request, filter_settings) + + filter_settings_2d = filter_settings.copy() + filter_settings_2d['start_date'] = filter_settings['start_date'] - \ + timedelta(days=2) + filter_settings_2d['end_date'] = filter_settings['end_date'] - \ + timedelta(days=2) + result_2d = RequestMetricService.get_metrics_stats( + request, filter_settings_2d) + + filter_settings_7d = filter_settings.copy() + filter_settings_7d['start_date'] = filter_settings['start_date'] - \ + timedelta(days=7) + filter_settings_7d['end_date'] = filter_settings['end_date'] - \ + timedelta(days=7) + result_7d = RequestMetricService.get_metrics_stats( + request, filter_settings_7d) + + plot_data = [] + + for item in result_now: + point = {'x': item['x'], 'today': 0, 'days_ago_2': 0, + 'days_ago_7': 0} + if item['requests']: + point['today'] = round(item['main'] / item['requests'], 3) + plot_data.append(point) + + for i, item in enumerate(result_2d[:len(plot_data)]): + plot_data[i]['days_ago_2'] = 0 + point = result_2d[i] + if point['requests']: + plot_data[i]['days_ago_2'] = round(point['main'] / + point['requests'], 3) + + for i, item in enumerate(result_7d[:len(plot_data)]): + plot_data[i]['days_ago_7'] = 0 + point = result_7d[i] + if point['requests']: + plot_data[i]['days_ago_7'] = round(point['main'] / + point['requests'], 3) + + return plot_data + + +@view_config(route_name='applications_property', + match_param='key=requests_graphs', renderer='json', + permission='view') +def requests_graphs(request): + """ + Handles dashboard infomation for avg. response time split by today, + 2 days ago and week ago + """ + query_params = request.GET.mixed() + query_params['resource'] = (request.context.resource.resource_id,) + + filter_settings = build_filter_settings_from_query_dict(request, + query_params) + + if not filter_settings.get('end_date'): + end_date = datetime.utcnow().replace(microsecond=0, second=0) + filter_settings['end_date'] = end_date + + delta = timedelta(hours=1) + if not filter_settings.get('start_date'): + filter_settings['start_date'] = filter_settings['end_date'] - delta + + result_now = RequestMetricService.get_metrics_stats( + request, filter_settings) + + delta = filter_settings['end_date'] - filter_settings['start_date'] + if delta < h.time_deltas.get('12h')['delta']: + seconds = h.time_deltas['1m']['minutes'] * 60.0 + elif delta <= h.time_deltas.get('3d')['delta']: + seconds = h.time_deltas['5m']['minutes'] * 60.0 + elif delta >= h.time_deltas.get('2w')['delta']: + seconds = h.time_deltas['24h']['minutes'] * 60.0 + else: + seconds = h.time_deltas['1h']['minutes'] * 60.0 + + for item in result_now: + if item['requests']: + item['requests'] = round(item['requests'] / seconds, 3) + return result_now + + +@view_config(route_name='applications_property', + match_param='key=apdex_stats', renderer='json', + permission='view') +def get_apdex_stats(request): + """ + Returns information and calculates APDEX score per server for dashboard + server information (upper right stats boxes) + """ + query_params = request.GET.mixed() + query_params['resource'] = (request.context.resource.resource_id,) + + filter_settings = build_filter_settings_from_query_dict(request, + query_params) + # make sure we have only one resource here to don't produce + # weird results when we have wrong app in app selector + filter_settings['resource'] = [filter_settings['resource'][0]] + + if not filter_settings.get('end_date'): + end_date = datetime.utcnow().replace(microsecond=0, second=0) + filter_settings['end_date'] = end_date + + delta = timedelta(hours=1) + if not filter_settings.get('start_date'): + filter_settings['start_date'] = filter_settings['end_date'] - delta + + return RequestMetricService.get_apdex_stats(request, filter_settings) + + +@view_config(route_name='applications_property', match_param='key=slow_calls', + renderer='json', permission='view') +def get_slow_calls(request): + """ + Returns information for time consuming calls in specific time interval + """ + query_params = request.GET.mixed() + query_params['resource'] = (request.context.resource.resource_id,) + + filter_settings = build_filter_settings_from_query_dict(request, + query_params) + + if not filter_settings.get('end_date'): + end_date = datetime.utcnow().replace(microsecond=0, second=0) + filter_settings['end_date'] = end_date + + delta = timedelta(hours=1) + if not filter_settings.get('start_date'): + filter_settings['start_date'] = filter_settings['end_date'] - delta + + return SlowCallService.get_time_consuming_calls(request, filter_settings) + + +@view_config(route_name='applications_property', + match_param='key=requests_breakdown', + renderer='json', permission='view') +def get_requests_breakdown(request): + """ + Used on dashboard to get information which views are most used in + a time interval + """ + query_params = request.GET.mixed() + query_params['resource'] = (request.context.resource.resource_id,) + + filter_settings = build_filter_settings_from_query_dict(request, + query_params) + if not filter_settings.get('end_date'): + end_date = datetime.utcnow().replace(microsecond=0, second=0) + filter_settings['end_date'] = end_date + + if not filter_settings.get('start_date'): + delta = timedelta(hours=1) + filter_settings['start_date'] = filter_settings['end_date'] - delta + + series = RequestMetricService.get_requests_breakdown( + request, filter_settings) + + results = [] + for row in series: + d_row = {'avg_response': round(row['main'] / row['requests'], 3), + 'requests': row['requests'], + 'main': row['main'], + 'view_name': row['key'], + 'latest_details': row['latest_details'], + 'percentage': round(row['percentage'] * 100, 1)} + + results.append(d_row) + + return results + + +@view_config(route_name='applications_property', + match_param='key=trending_reports', renderer='json', + permission='view') +def trending_reports(request): + """ + Returns exception/slow reports trending for specific time interval + """ + query_params = request.GET.mixed().copy() + # pop report type to rewrite it to tag later + report_type = query_params.pop('report_type', None) + if report_type: + query_params['type'] = report_type + + query_params['resource'] = (request.context.resource.resource_id,) + + filter_settings = build_filter_settings_from_query_dict(request, + query_params) + + if not filter_settings.get('end_date'): + end_date = datetime.utcnow().replace(microsecond=0, second=0) + filter_settings['end_date'] = end_date + + if not filter_settings.get('start_date'): + delta = timedelta(hours=1) + filter_settings['start_date'] = filter_settings['end_date'] - delta + + results = ReportGroupService.get_trending(request, filter_settings) + + trending = [] + for occurences, group in results: + report_group = group.get_dict(request) + # show the occurences in time range instead of global ones + report_group['occurences'] = occurences + trending.append(report_group) + + return trending + + +@view_config(route_name='applications_property', + match_param='key=integrations', + renderer='json', permission='view') +def integrations(request): + """ + Integration list for given application + """ + application = request.context.resource + return {'resource': application} + + +@view_config(route_name='applications_property', + match_param='key=user_permissions', renderer='json', + permission='owner', request_method='POST') +def user_resource_permission_create(request): + """ + Set new permissions for user for a resource + """ + resource = request.context.resource + user_name = request.unsafe_json_body.get('user_name') + user = User.by_user_name(user_name) + if not user: + user = User.by_email(user_name) + if not user: + return False + + for perm_name in request.unsafe_json_body.get('permissions', []): + permission = UserResourcePermission.by_resource_user_and_perm( + user.id, perm_name, resource.resource_id) + if not permission: + permission = UserResourcePermission(perm_name=perm_name, + user_id=user.id) + resource.user_permissions.append(permission) + DBSession.flush() + perms = [p.perm_name for p in resource.perms_for_user(user) + if p.type == 'user'] + result = {'user_name': user.user_name, + 'permissions': list(set(perms))} + return result + + +@view_config(route_name='applications_property', + match_param='key=user_permissions', renderer='json', + permission='owner', request_method='DELETE') +def user_resource_permission_delete(request): + """ + Removes user permission from specific resource + """ + resource = request.context.resource + + user = User.by_user_name(request.GET.get('user_name')) + if not user: + return False + + for perm_name in request.GET.getall('permissions'): + permission = UserResourcePermission.by_resource_user_and_perm( + user.id, perm_name, resource.resource_id) + resource.user_permissions.remove(permission) + DBSession.flush() + perms = [p.perm_name for p in resource.perms_for_user(user) + if p.type == 'user'] + result = {'user_name': user.user_name, + 'permissions': list(set(perms))} + return result + + +@view_config(route_name='applications_property', + match_param='key=group_permissions', renderer='json', + permission='owner', request_method='POST') +def group_resource_permission_create(request): + """ + Set new permissions for group for a resource + """ + resource = request.context.resource + group = GroupService.by_id(request.unsafe_json_body.get('group_id')) + if not group: + return False + + for perm_name in request.unsafe_json_body.get('permissions', []): + permission = GroupResourcePermissionService.by_resource_group_and_perm( + group.id, perm_name, resource.resource_id) + if not permission: + permission = GroupResourcePermission(perm_name=perm_name, + group_id=group.id) + resource.group_permissions.append(permission) + DBSession.flush() + perm_tuples = resource.groups_for_perm( + ANY_PERMISSION, + limit_group_permissions=True, + group_ids=[group.id]) + perms = [p.perm_name for p in perm_tuples if p.type == 'group'] + result = {'group': group.get_dict(), + 'permissions': list(set(perms))} + return result + + +@view_config(route_name='applications_property', + match_param='key=group_permissions', renderer='json', + permission='owner', request_method='DELETE') +def group_resource_permission_delete(request): + """ + Removes group permission from specific resource + """ + form = forms.ReactorForm(request.POST, csrf_context=request) + form.validate() + resource = request.context.resource + group = GroupService.by_id(request.GET.get('group_id')) + if not group: + return False + + for perm_name in request.GET.getall('permissions'): + permission = GroupResourcePermissionService.by_resource_group_and_perm( + group.id, perm_name, resource.resource_id) + resource.group_permissions.remove(permission) + DBSession.flush() + perm_tuples = resource.groups_for_perm( + ANY_PERMISSION, + limit_group_permissions=True, + group_ids=[group.id]) + perms = [p.perm_name for p in perm_tuples if p.type == 'group'] + result = {'group': group.get_dict(), + 'permissions': list(set(perms))} + return result diff --git a/backend/src/appenlight/views/events.py b/backend/src/appenlight/views/events.py new file mode 100644 index 0000000..efafbc7 --- /dev/null +++ b/backend/src/appenlight/views/events.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +from appenlight.lib.helpers import gen_pagination_headers +from appenlight.models.services.event import EventService +from pyramid.view import view_config +from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound + + +@view_config(route_name='events_no_id', + renderer='json', permission='authenticated') +def fetch_events(request): + """ + Returns list of log entries from Elasticsearch + """ + event_paginator = EventService.get_paginator( + user=request.user, + page=1, + items_per_page=100 + ) + headers = gen_pagination_headers(request, event_paginator) + request.response.headers.update(headers) + + return [ev.get_dict() for ev in event_paginator.items] + + +@view_config(route_name='events', renderer='json', request_method='PATCH', + permission='authenticated') +def event_PATCH(request): + resources = request.user.resources_with_perms( + ['view'], resource_types=request.registry.resource_types) + event = EventService.for_resource( + [r.resource_id for r in resources], + event_id=request.matchdict['event_id']).first() + if not event: + return HTTPNotFound() + allowed_keys = ['status'] + for k, v in request.unsafe_json_body.items(): + if k in allowed_keys: + if k == 'status': + event.close() + else: + setattr(event, k, v) + else: + return HTTPBadRequest() + return event.get_dict() diff --git a/backend/src/appenlight/views/groups.py b/backend/src/appenlight/views/groups.py new file mode 100644 index 0000000..23710b6 --- /dev/null +++ b/backend/src/appenlight/views/groups.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import logging + +from pyramid.view import view_config +from pyramid.httpexceptions import HTTPUnprocessableEntity, HTTPNotFound + +from appenlight.lib.utils import permission_tuple_to_dict +from appenlight.models.services.config import ConfigService +from appenlight.models.group import Group +from appenlight.models.services.group import GroupService +from appenlight.models.user import User +from appenlight.models import DBSession +from appenlight import forms +from webob.multidict import MultiDict + +log = logging.getLogger(__name__) + +_ = str + + +@view_config(route_name='groups_no_id', renderer='json', + request_method="GET", permission='authenticated') +def groups_list(request): + """ + Returns groups list + """ + groups = Group.all().order_by(Group.group_name) + list_groups = ConfigService.by_key_and_section( + 'list_groups_to_non_admins', 'global') + if list_groups or request.has_permission('root_administration'): + return [g.get_dict() for g in groups] + else: + return [] + + +@view_config(route_name='groups_no_id', renderer='json', + request_method="POST", permission='root_administration') +def groups_create(request): + """ + Returns groups list + """ + form = forms.GroupCreateForm( + MultiDict(request.safe_json_body or {}), csrf_context=request) + if form.validate(): + log.info('registering group') + group = Group() + # insert new group here + DBSession.add(group) + form.populate_obj(group) + request.session.flash(_('Group created')) + DBSession.flush() + return group.get_dict(include_perms=True) + else: + return HTTPUnprocessableEntity(body=form.errors_json) + + +@view_config(route_name='groups', renderer='json', + request_method="DELETE", permission='root_administration') +def groups_DELETE(request): + """ + Removes a groups permanently from db + """ + msg = _('You cannot remove administrator group from the system') + group = GroupService.by_id(request.matchdict.get('group_id')) + if group: + if group.id == 1: + request.session.flash(msg, 'warning') + else: + DBSession.delete(group) + request.session.flash(_('Group removed')) + return True + request.response.status = 422 + return False + + +@view_config(route_name='groups', renderer='json', + request_method="GET", permission='root_administration') +@view_config(route_name='groups', renderer='json', + request_method="PATCH", permission='root_administration') +def group_update(request): + """ + Updates group object + """ + group = GroupService.by_id(request.matchdict.get('group_id')) + if not group: + return HTTPNotFound() + + if request.method == 'PATCH': + form = forms.GroupCreateForm( + MultiDict(request.unsafe_json_body), csrf_context=request) + form._modified_group = group + if form.validate(): + form.populate_obj(group) + else: + return HTTPUnprocessableEntity(body=form.errors_json) + return group.get_dict(include_perms=True) + + +@view_config(route_name='groups_property', + match_param='key=resource_permissions', + renderer='json', permission='root_administration') +def groups_resource_permissions_list(request): + """ + Get list of permissions assigned to specific resources + """ + group = GroupService.by_id(request.matchdict.get('group_id')) + if not group: + return HTTPNotFound() + return [permission_tuple_to_dict(perm) for perm in + group.resources_with_possible_perms()] + + +@view_config(route_name='groups_property', + match_param='key=users', request_method="GET", + renderer='json', permission='root_administration') +def groups_users_list(request): + """ + Get list of permissions assigned to specific resources + """ + group = GroupService.by_id(request.matchdict.get('group_id')) + if not group: + return HTTPNotFound() + props = ['user_name', 'id', 'first_name', 'last_name', 'email', + 'last_login_date', 'status'] + users_dicts = [] + for user in group.users: + u_dict = user.get_dict(include_keys=props) + u_dict['gravatar_url'] = user.gravatar_url(s=20) + users_dicts.append(u_dict) + return users_dicts + + +@view_config(route_name='groups_property', + match_param='key=users', request_method="DELETE", + renderer='json', permission='root_administration') +def groups_users_remove(request): + """ + Get list of permissions assigned to specific resources + """ + group = GroupService.by_id(request.matchdict.get('group_id')) + user = User.by_user_name(request.GET.get('user_name')) + if not group or not user: + return HTTPNotFound() + if len(group.users) > 1: + group.users.remove(user) + msg = "User removed from group" + request.session.flash(msg) + group.member_count = group.users_dynamic.count() + return True + msg = "Administrator group needs to contain at least one user" + request.session.flash(msg, 'warning') + return False + + +@view_config(route_name='groups_property', + match_param='key=users', request_method="POST", + renderer='json', permission='root_administration') +def groups_users_add(request): + """ + Get list of permissions assigned to specific resources + """ + group = GroupService.by_id(request.matchdict.get('group_id')) + user = User.by_user_name(request.unsafe_json_body.get('user_name')) + if not user: + user = User.by_email(request.unsafe_json_body.get('user_name')) + + if not group or not user: + return HTTPNotFound() + if user not in group.users: + group.users.append(user) + group.member_count = group.users_dynamic.count() + props = ['user_name', 'id', 'first_name', 'last_name', 'email', + 'last_login_date', 'status'] + u_dict = user.get_dict(include_keys=props) + u_dict['gravatar_url'] = user.gravatar_url(s=20) + return u_dict diff --git a/backend/src/appenlight/views/index.py b/backend/src/appenlight/views/index.py new file mode 100644 index 0000000..960ce42 --- /dev/null +++ b/backend/src/appenlight/views/index.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import datetime +import logging +import uuid + +import pyramid.security as security + +from pyramid.view import view_config +from pyramid.httpexceptions import HTTPFound +from pyramid.response import Response +from pyramid.security import NO_PERMISSION_REQUIRED +from ziggurat_foundations.ext.pyramid.sign_in import ZigguratSignInSuccess +from ziggurat_foundations.ext.pyramid.sign_in import ZigguratSignInBadAuth +from ziggurat_foundations.ext.pyramid.sign_in import ZigguratSignOut + +from appenlight.lib.social import handle_social_data +from appenlight.models import DBSession +from appenlight.models.user import User +from appenlight.models.services.user import UserService +from appenlight.subscribers import _ +from appenlight import forms +from webob.multidict import MultiDict + +log = logging.getLogger(__name__) + + +@view_config(context=ZigguratSignInSuccess, permission=NO_PERMISSION_REQUIRED) +def sign_in(request): + """ + Performs sign in by sending proper user identification headers + Regenerates CSRF token + """ + user = request.context.user + if user.status == 1: + request.session.new_csrf_token() + user.last_login_date = datetime.datetime.utcnow() + social_data = request.session.get('zigg.social_auth') + if social_data: + handle_social_data(request, user, social_data) + else: + request.session.flash(_('Account got disabled')) + + if request.context.came_from != '/': + return HTTPFound(location=request.context.came_from, + headers=request.context.headers) + else: + return HTTPFound(location=request.route_url('/'), + headers=request.context.headers) + + +@view_config(context=ZigguratSignInBadAuth, permission=NO_PERMISSION_REQUIRED) +def bad_auth(request): + """ + Handles incorrect login flow + """ + request.session.flash(_('Incorrect username or password'), 'warning') + return HTTPFound(location=request.route_url('register'), + headers=request.context.headers) + + +@view_config(context=ZigguratSignOut, permission=NO_PERMISSION_REQUIRED) +def sign_out(request): + """ + Removes user identification cookie + """ + return HTTPFound(location=request.route_url('register'), + headers=request.context.headers) + + +@view_config(route_name='lost_password', + renderer='appenlight:templates/user/lost_password.jinja2', + permission=NO_PERMISSION_REQUIRED) +def lost_password(request): + """ + Presents lost password page - sends password reset link to + specified email address. + This link is valid only for 10 minutes + """ + form = forms.LostPasswordForm(request.POST, csrf_context=request) + if request.method == 'POST' and form.validate(): + user = User.by_email(form.email.data) + if user: + user.regenerate_security_code() + user.security_code_date = datetime.datetime.utcnow() + email_vars = { + 'user': user, + 'request': request, + 'email_title': "App Enlight :: New password request" + } + UserService.send_email( + request, recipients=[user.email], + variables=email_vars, + template='/email_templates/lost_password.jinja2') + msg = 'Password reset email had been sent. ' \ + 'Please check your mailbox for further instructions.' + request.session.flash(_(msg)) + return HTTPFound(location=request.route_url('lost_password')) + return {"form": form} + + +@view_config(route_name='lost_password_generate', + permission=NO_PERMISSION_REQUIRED, + renderer='appenlight:templates/user/lost_password_generate.jinja2') +def lost_password_generate(request): + """ + Shows new password form - perform time check and set new password for user + """ + user = User.by_user_name_and_security_code( + request.GET.get('user_name'), request.GET.get('security_code')) + if user: + delta = datetime.datetime.utcnow() - user.security_code_date + + if user and delta.total_seconds() < 600: + form = forms.NewPasswordForm(request.POST, csrf_context=request) + if request.method == "POST" and form.validate(): + user.set_password(form.new_password.data) + request.session.flash(_('You can sign in with your new password.')) + return HTTPFound(location=request.route_url('register')) + else: + return {"form": form} + else: + return Response('Security code expired') + + +@view_config(route_name='register', + renderer='appenlight:templates/user/register.jinja2', + permission=NO_PERMISSION_REQUIRED) +def register(request): + """ + Render register page with form + Also handles oAuth flow for registration + """ + login_url = request.route_url('ziggurat.routes.sign_in') + if request.query_string: + query_string = '?%s' % request.query_string + else: + query_string = '' + referrer = '%s%s' % (request.path, query_string) + + if referrer in [login_url, '/register', '/register?sign_in=1']: + referrer = '/' # never use the login form itself as came_from + sign_in_form = forms.SignInForm( + came_from=request.params.get('came_from', referrer), + csrf_context=request) + + # populate form from oAuth session data returned by authomatic + social_data = request.session.get('zigg.social_auth') + if request.method != 'POST' and social_data: + log.debug(social_data) + user_name = social_data['user'].get('user_name', '').split('@')[0] + form_data = { + 'user_name': user_name, + 'email': social_data['user'].get('email') + } + form_data['user_password'] = str(uuid.uuid4()) + form = forms.UserRegisterForm(MultiDict(form_data), + csrf_context=request) + form.user_password.widget.hide_value = False + else: + form = forms.UserRegisterForm(request.POST, csrf_context=request) + if request.method == 'POST' and form.validate(): + log.info('registering user') + # insert new user here + new_user = User() + DBSession.add(new_user) + form.populate_obj(new_user) + new_user.regenerate_security_code() + new_user.status = 1 + new_user.set_password(new_user.user_password) + new_user.registration_ip = request.environ.get('REMOTE_ADDR') + + if social_data: + handle_social_data(request, new_user, social_data) + + email_vars = {'user': new_user, + 'request': request, + 'email_title': "App Enlight :: Start information"} + UserService.send_email( + request, recipients=[new_user.email], variables=email_vars, + template='/email_templates/registered.jinja2') + request.session.flash(_('You have successfully registered.')) + DBSession.flush() + headers = security.remember(request, new_user.id) + return HTTPFound(location=request.route_url('/'), + headers=headers) + return { + "form": form, + "sign_in_form": sign_in_form + } + + +@view_config(route_name='/', + renderer='appenlight:templates/dashboard/index.jinja2', + permission=NO_PERMISSION_REQUIRED) +@view_config(route_name='angular_app_ui', + renderer='appenlight:templates/dashboard/index.jinja2', + permission=NO_PERMISSION_REQUIRED) +@view_config(route_name='angular_app_ui_ix', + renderer='appenlight:templates/dashboard/index.jinja2', + permission=NO_PERMISSION_REQUIRED) +def app_main_index(request): + """ + Render dashoard/report browser page page along with: + - flash messages + - application list + - assigned reports + - latest events + (those last two come from subscribers.py that sets global renderer variables) + """ + + if request.user: + request.user.last_login_date = datetime.datetime.utcnow() + applications = request.user.resources_with_perms( + ['view'], resource_types=['application']) + # convert for angular + applications = dict( + [(a.resource_id, a.resource_name) for a in applications.all()] + ) + else: + applications = {} + return { + 'applications': applications + } diff --git a/backend/src/appenlight/views/integrations/__init__.py b/backend/src/appenlight/views/integrations/__init__.py new file mode 100644 index 0000000..f5743ef --- /dev/null +++ b/backend/src/appenlight/views/integrations/__init__.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import logging +import random +from datetime import datetime + +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config + +from appenlight.models import DBSession +from appenlight.models.event import Event +from appenlight.models.integrations import IntegrationBase +from appenlight.models.alert_channel import AlertChannel +from appenlight.models.report_group import ReportGroup +from appenlight.models.services.alert_channel import AlertChannelService +from appenlight.lib import generate_random_string + +log = logging.getLogger(__name__) + +dummy_report = ReportGroup() +dummy_report.error = "ProtocolError: ('Connection aborted.', " \ + "error(111, 'Connection refused'))" +dummy_report.total_reports = 4 +dummy_report.occurences = 4 + +dummy_report2 = ReportGroup() +dummy_report2.error = "UnboundLocalError: local variable " \ + "'hits' referenced before assignment" +dummy_report2.total_reports = 8 +dummy_report2.occurences = 8 + +dummy_reports = [(4, dummy_report), (8, dummy_report2)] + + +class IntegrationView(object): + """ + Base class for 3rd party integrations setup views + """ + + def __init__(self, request): + self.request = request + resource = self.request.context.resource + integration_name = request.matchdict['integration'] + integration = IntegrationBase.by_app_id_and_integration_name( + resource.resource_id, integration_name) + if integration: + dict_config = integration.config + else: + dict_config = {} + self.integration = integration + self.integration_config = dict_config + + @view_config(route_name='integrations_id', + request_method="DELETE", + renderer='json', + permission='edit') + def remove_integration(self): + if self.integration: + DBSession.delete(self.integration) + self.request.session.flash('Integration removed') + return '' + + @view_config(route_name='integrations_id', + request_method="POST", + match_param=['action=test_report_notification'], + renderer='json', + permission='edit') + def test_report_notification(self): + if not self.integration: + self.request.session.flash('Integration needs to be configured', + 'warning') + return False + + resource = self.integration.resource + + channel = AlertChannelService.by_integration_id(self.integration.id) + + if random.random() < 0.5: + confirmed_reports = dummy_reports + else: + confirmed_reports = [random.choice(dummy_reports)] + + channel.notify_reports(resource=resource, + user=self.request.user, + request=self.request, + since_when=datetime.utcnow(), + reports=confirmed_reports) + self.request.session.flash('Report notification sent') + return True + + @view_config(route_name='integrations_id', + request_method="POST", + match_param=['action=test_error_alert'], + renderer='json', + permission='edit') + def test_error_alert(self): + if not self.integration: + self.request.session.flash('Integration needs to be configured', + 'warning') + return False + + resource = self.integration.resource + + event_name = random.choice(('error_report_alert', + 'slow_report_alert',)) + new_event = Event(resource_id=resource.resource_id, + event_type=Event.types[event_name], + start_date=datetime.utcnow(), + status=Event.statuses['active'], + values={'reports': random.randint(11, 99), + 'threshold': 10} + ) + + channel = AlertChannelService.by_integration_id(self.integration.id) + + channel.notify_alert(resource=resource, + event=new_event, + user=self.request.user, + request=self.request) + self.request.session.flash('Notification sent') + return True + + @view_config(route_name='integrations_id', + request_method="POST", + match_param=['action=test_daily_digest'], + renderer='json', + permission='edit') + def test_daily_digest(self): + if not self.integration: + self.request.session.flash('Integration needs to be configured', + 'warning') + return False + + resource = self.integration.resource + channel = AlertChannelService.by_integration_id(self.integration.id) + + channel.send_digest(resource=resource, + user=self.request.user, + request=self.request, + since_when=datetime.utcnow(), + reports=dummy_reports) + self.request.session.flash('Notification sent') + return True + + @view_config(route_name='integrations_id', + request_method="POST", + match_param=['action=test_uptime_alert'], + renderer='json', + permission='edit') + def test_uptime_alert(self): + if not self.integration: + self.request.session.flash('Integration needs to be configured', + 'warning') + return False + + resource = self.integration.resource + + new_event = Event(resource_id=resource.resource_id, + event_type=Event.types['uptime_alert'], + start_date=datetime.utcnow(), + status=Event.statuses['active'], + values={"status_code": 500, + "tries": 2, + "response_time": 0}) + + channel = AlertChannelService.by_integration_id(self.integration.id) + channel.notify_uptime_alert(resource=resource, + event=new_event, + user=self.request.user, + request=self.request) + + self.request.session.flash('Notification sent') + return True + + @view_config(route_name='integrations_id', + request_method="POST", + match_param=['action=test_chart_alert'], + renderer='json', + permission='edit') + def test_chart_alert(self): + if not self.integration: + self.request.session.flash('Integration needs to be configured', + 'warning') + return False + + resource = self.integration.resource + + chart_values = { + "matched_rule": {'name': 'Fraud attempt limit'}, + "matched_step_values": {"labels": { + "0_1": {"human_label": "Attempts sum"}}, + "values": {"0_1": random.randint(11, 55), + "key": "2015-12-16T15:49:00"}}, + "start_interval": datetime.utcnow(), + "resource": 1, + "chart_name": "Fraud attempts per day", + "chart_uuid": "some_uuid", + "step_size": 3600, + "action_name": "Notify excessive fraud attempts"} + + new_event = Event(resource_id=resource.resource_id, + event_type=Event.types['chart_alert'], + status=Event.statuses['active'], + values=chart_values, + target_uuid="some_uuid", + start_date=datetime.utcnow()) + + channel = AlertChannelService.by_integration_id(self.integration.id) + channel.notify_chart_alert(resource=resource, + event=new_event, + user=self.request.user, + request=self.request) + + self.request.session.flash('Notification sent') + return True + + def create_missing_channel(self, resource, channel_name): + """ + Recreates alert channel for the owner of integration + """ + if self.integration: + if not self.integration.channel: + channel = AlertChannel() + channel.channel_name = channel_name + channel.channel_validated = True + channel.channel_value = resource.resource_id + channel.integration_id = self.integration.id + security_code = generate_random_string(10) + channel.channel_json_conf = {'security_code': security_code} + resource.owner.alert_channels.append(channel) diff --git a/backend/src/appenlight/views/integrations/bitbucket.py b/backend/src/appenlight/views/integrations/bitbucket.py new file mode 100644 index 0000000..38c9534 --- /dev/null +++ b/backend/src/appenlight/views/integrations/bitbucket.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +from appenlight.models.integrations.bitbucket import BitbucketIntegration, \ + IntegrationException +from appenlight.models.report_comment import ReportComment +from appenlight.models.services.report_group import ReportGroupService +from pyramid.view import view_config +from appenlight import forms +import logging +from datetime import datetime +from pyramid.httpexceptions import HTTPUnprocessableEntity +from webob.multidict import MultiDict + +log = logging.getLogger(__name__) + +from . import IntegrationView + + +class BitbucketView(IntegrationView): + @view_config(route_name='integrations_id', + match_param=['action=info', 'integration=bitbucket'], + renderer='json') + def get_bitbucket_info(self): + """ + Grab information about possible priority levels and assignable users + """ + try: + client = BitbucketIntegration.create_client( + self.request, + self.integration.config['user_name'], + self.integration.config['repo_name']) + except IntegrationException as e: + self.request.response.status_code = 503 + return {'error_messages': [str(e)]} + assignees = client.get_assignees() + priorities = client.get_priorities() + return {'assignees': assignees, + 'priorities': priorities} + + @view_config(route_name='integrations_id', + match_param=['action=create-issue', + 'integration=bitbucket'], + renderer='json') + def create_issue(self): + """ + Creates a new issue in bitbucket issue tracker from report group + """ + report = ReportGroupService.by_id( + self.request.unsafe_json_body['group_id']) + form_data = { + 'title': self.request.unsafe_json_body.get('title', + 'Unknown Title'), + 'content': self.request.unsafe_json_body.get('content', ''), + 'kind': 'bug', + 'priority': self.request.unsafe_json_body['priority'], + 'responsible': self.request.unsafe_json_body['responsible']['user'] + } + + try: + client = BitbucketIntegration.create_client( + self.request, + self.integration.config['user_name'], + self.integration.config['repo_name']) + issue = client.create_issue(form_data) + except IntegrationException as e: + self.request.response.status_code = 503 + return {'error_messages': [str(e)]} + + comment_body = 'Bitbucket issue created: %s ' % issue['web_url'] + comment = ReportComment(user_name=self.request.user.user_name, + report_time=report.first_timestamp, + body=comment_body) + report.comments.append(comment) + return True + + @view_config(route_name='integrations_id', + match_param=['action=setup', 'integration=bitbucket'], + renderer='json', + permission='edit') + def setup(self): + """ + Validates and creates integration between application and bitbucket + """ + resource = self.request.context.resource + form = forms.IntegrationBitbucketForm( + MultiDict(self.request.safe_json_body or {}), + csrf_context=self.request, **self.integration_config) + if self.request.method == 'POST' and form.validate(): + integration_config = { + 'repo_name': form.repo_name.data, + 'user_name': form.user_name.data, + 'host_name': 'https://bitbucket.org' + } + if not self.integration: + # add new integration + self.integration = BitbucketIntegration( + modified_date=datetime.utcnow(), + ) + self.request.session.flash('Integration added') + resource.integrations.append(self.integration) + else: + self.request.session.flash('Integration updated') + self.integration.config = integration_config + return integration_config + elif self.request.method == 'POST': + return HTTPUnprocessableEntity(body=form.errors_json) + return self.integration_config diff --git a/backend/src/appenlight/views/integrations/campfire.py b/backend/src/appenlight/views/integrations/campfire.py new file mode 100644 index 0000000..5aad746 --- /dev/null +++ b/backend/src/appenlight/views/integrations/campfire.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +from ...models import DBSession +from ...models.integrations.campfire import CampfireIntegration, \ + IntegrationException +from ...models.alert_channel import AlertChannel +from ...lib import generate_random_string +from pyramid.httpexceptions import HTTPFound, HTTPUnprocessableEntity +from pyramid.view import view_config +from ... import forms +import logging +from datetime import datetime +from webob.multidict import MultiDict + +log = logging.getLogger(__name__) + +from . import IntegrationView + + +class CampfireView(IntegrationView): + @view_config(route_name='integrations_id', + match_param=['action=info', 'integration=campfire'], + renderer='json') + def get_info(self): + pass + + @view_config(route_name='integrations_id', + match_param=['action=setup', 'integration=campfire'], + renderer='json', + permission='edit') + def setup(self): + """ + Validates and creates integration between application and campfire + """ + resource = self.request.context.resource + self.create_missing_channel(resource, 'campfire') + + form = forms.IntegrationCampfireForm( + MultiDict(self.request.safe_json_body or {}), + csrf_context=self.request, + **self.integration_config) + + if self.request.method == 'POST' and form.validate(): + integration_config = { + 'account': form.account.data, + 'api_token': form.api_token.data, + 'rooms': form.rooms.data, + } + if not self.integration: + # add new integration + self.integration = CampfireIntegration( + modified_date=datetime.utcnow(), + ) + self.request.session.flash('Integration added') + resource.integrations.append(self.integration) + else: + self.request.session.flash('Integration updated') + self.integration.config = integration_config + DBSession.flush() + self.create_missing_channel(resource, 'campfire') + return integration_config + elif self.request.method == 'POST': + return HTTPUnprocessableEntity(body=form.errors_json) + return self.integration_config diff --git a/backend/src/appenlight/views/integrations/flowdock.py b/backend/src/appenlight/views/integrations/flowdock.py new file mode 100644 index 0000000..e13a432 --- /dev/null +++ b/backend/src/appenlight/views/integrations/flowdock.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +from appenlight.models import DBSession +from appenlight.models.integrations.flowdock import FlowdockIntegration +from pyramid.httpexceptions import HTTPUnprocessableEntity +from pyramid.view import view_config +from appenlight import forms +import logging +from datetime import datetime +from webob.multidict import MultiDict + +log = logging.getLogger(__name__) + +from . import IntegrationView + + +class FlowdockView(IntegrationView): + @view_config(route_name='integrations_id', + match_param=['action=info', 'integration=flowdock'], + renderer='json') + def get_info(self): + pass + + @view_config(route_name='integrations_id', + match_param=['action=setup', 'integration=flowdock'], + renderer='json', + permission='edit') + def setup(self): + """ + Validates and creates integration between application and flowdock + """ + resource = self.request.context.resource + self.create_missing_channel(resource, 'flowdock') + + form = forms.IntegrationFlowdockForm( + MultiDict(self.request.safe_json_body or {}), + csrf_context=self.request, **self.integration_config) + if self.request.method == 'POST' and form.validate(): + integration_config = { + 'api_token': form.api_token.data, + } + if not self.integration: + # add new integration + self.integration = FlowdockIntegration( + modified_date=datetime.utcnow(), + ) + self.request.session.flash('Integration added') + resource.integrations.append(self.integration) + else: + self.request.session.flash('Integration updated') + self.integration.config = integration_config + DBSession.flush() + self.create_missing_channel(resource, 'flowdock') + return integration_config + elif self.request.method == 'POST': + return HTTPUnprocessableEntity(body=form.errors_json) + return self.integration_config diff --git a/backend/src/appenlight/views/integrations/github.py b/backend/src/appenlight/views/integrations/github.py new file mode 100644 index 0000000..66375c0 --- /dev/null +++ b/backend/src/appenlight/views/integrations/github.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +from appenlight.models import DBSession +from appenlight.models.integrations.github import GithubIntegration, \ + IntegrationException +from appenlight.models.report_comment import ReportComment +from appenlight.models.services.report_group import ReportGroupService +from pyramid.view import view_config +from appenlight import forms +import logging +from datetime import datetime +from pyramid.httpexceptions import HTTPFound, HTTPUnprocessableEntity +from webob.multidict import MultiDict + +log = logging.getLogger(__name__) + +from . import IntegrationView + + +class GithubView(IntegrationView): + @view_config(route_name='integrations_id', + match_param=['action=info', 'integration=github'], + renderer='json') + def get_github_info(self): + """ + Grab information about possible priority statuses and assignable users + """ + try: + client = GithubIntegration.create_client( + self.request, + self.integration.config['user_name'], + self.integration.config['repo_name']) + except IntegrationException as e: + self.request.response.status_code = 503 + return {'error_messages': [str(e)]} + try: + assignees = client.get_assignees() + statuses = client.get_statuses() + except IntegrationException as e: + return {'error_messages': [str(e)]} + return {'assignees': assignees, + 'statuses': statuses} + + @view_config(route_name='integrations_id', + match_param=['action=create-issue', 'integration=github'], + renderer='json') + def create_issue(self): + """ + Creates a new issue in github issue tracker from report group + """ + report = ReportGroupService.by_id( + self.request.unsafe_json_body['group_id']) + form_data = { + 'title': self.request.unsafe_json_body.get('title', + 'Unknown Title'), + 'content': self.request.unsafe_json_body.get('content'), + 'kind': [self.request.unsafe_json_body['status']], + 'responsible': self.request.unsafe_json_body['responsible']['user'] + } + + try: + client = GithubIntegration.create_client( + self.request, + self.integration.config['user_name'], + self.integration.config['repo_name']) + issue = client.create_issue(form_data) + except IntegrationException as e: + self.request.response.status_code = 503 + return {'error_messages': [str(e)]} + + comment_body = 'Github issue created: %s ' % issue['web_url'] + comment = ReportComment(user_name=self.request.user.user_name, + report_time=report.first_timestamp, + body=comment_body) + report.comments.append(comment) + return True + + @view_config(route_name='integrations_id', + match_param=['action=setup', 'integration=github'], + renderer='json', + permission='edit') + def setup(self): + """ + Validates and creates integration between application and github + """ + resource = self.request.context.resource + form = forms.IntegrationGithubForm( + MultiDict(self.request.safe_json_body or {}), + csrf_context=self.request, + **self.integration_config) + if self.request.method == 'POST' and form.validate(): + integration_config = { + 'repo_name': form.repo_name.data, + 'user_name': form.user_name.data, + 'host_name': 'https://api.github.com' + } + if not self.integration: + self.integration = GithubIntegration( + modified_date=datetime.utcnow(), + + ) + self.integration.config = integration_config + resource.integrations.append(self.integration) + self.request.session.flash('Integration updated') + return integration_config + elif self.request.method == 'POST': + return HTTPUnprocessableEntity(body=form.errors_json) + return self.integration_config diff --git a/backend/src/appenlight/views/integrations/hipchat.py b/backend/src/appenlight/views/integrations/hipchat.py new file mode 100644 index 0000000..9c2d651 --- /dev/null +++ b/backend/src/appenlight/views/integrations/hipchat.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import logging +from datetime import datetime + +from appenlight.models import DBSession +from appenlight.models.integrations.hipchat import HipchatIntegration +from pyramid.httpexceptions import HTTPUnprocessableEntity +from pyramid.view import view_config +from appenlight import forms + +from webob.multidict import MultiDict + +log = logging.getLogger(__name__) + +from . import IntegrationView + + +class HipchatView(IntegrationView): + @view_config(route_name='integrations_id', + match_param=['action=info', 'integration=hipchat'], + renderer='json') + def get_info(self): + pass + + @view_config(route_name='integrations_id', + match_param=['action=setup', 'integration=hipchat'], + renderer='json', + permission='edit') + def setup(self): + """ + Validates and creates integration between application and hipchat + """ + resource = self.request.context.resource + self.create_missing_channel(resource, 'hipchat') + form = forms.IntegrationHipchatForm( + MultiDict(self.request.safe_json_body or {}), + csrf_context=self.request, **self.integration_config) + if self.request.method == 'POST' and form.validate(): + integration_config = { + 'api_token': form.api_token.data, + 'rooms': form.rooms.data, + } + if not self.integration: + # add new integration + self.integration = HipchatIntegration( + modified_date=datetime.utcnow(), + ) + self.request.session.flash('Integration added') + resource.integrations.append(self.integration) + else: + self.request.session.flash('Integration updated') + self.integration.config = integration_config + DBSession.flush() + self.create_missing_channel(resource, 'hipchat') + return integration_config + elif self.request.method == 'POST': + return HTTPUnprocessableEntity(body=form.errors_json) + return self.integration_config diff --git a/backend/src/appenlight/views/integrations/jira.py b/backend/src/appenlight/views/integrations/jira.py new file mode 100644 index 0000000..cfe9b9c --- /dev/null +++ b/backend/src/appenlight/views/integrations/jira.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import copy +import logging +from appenlight.models.integrations.jira import JiraIntegration, \ + JiraClient, IntegrationException +from appenlight.models.report_comment import ReportComment +from appenlight.models.services.report_group import ReportGroupService +from pyramid.view import view_config +from appenlight import forms +from datetime import datetime +from pyramid.httpexceptions import HTTPFound, HTTPUnprocessableEntity +from webob.multidict import MultiDict + +log = logging.getLogger(__name__) + +from . import IntegrationView + + +class JiraView(IntegrationView): + def create_client(self, form=None): + """ + Creates a client that can make authenticated requests to jira + """ + if self.integration and not form: + user_name = self.integration.config['user_name'] + password = self.integration.config['password'] + host_name = self.integration.config['host_name'] + project = self.integration.config['project'] + else: + user_name, password = form.user_name.data, form.password.data + host_name = form.host_name.data + project = form.host_name.data + client = JiraClient(user_name, password, host_name, project, + request=self.request) + return client + + @view_config(route_name='integrations_id', + match_param=['action=info', 'integration=jira'], + renderer='json') + def get_jira_info(self): + """ + Get basic metadata - assignees and priority levels from tracker + """ + try: + client = self.create_client() + except IntegrationException as e: + self.request.response.status_code = 503 + return {'error_messages': [str(e)]} + assignees = [] + priorities = [] + metadata = client.get_metadata() + for issue_type in metadata: + for field in issue_type['fields']: + if field['id'] == 'assignee': + assignees = field['values'] + if field['id'] == 'priority': + priorities = field['values'] + return {'assignees': assignees, + 'priorities': priorities} + + @view_config(route_name='integrations_id', + match_param=['action=create-issue', + 'integration=jira'], + renderer='json') + def create_issue(self): + """ + Creates a new issue in jira from report group + """ + report = ReportGroupService.by_id( + self.request.unsafe_json_body['group_id']) + form_data = { + 'title': self.request.unsafe_json_body.get('title', + 'Unknown Title'), + 'content': self.request.unsafe_json_body.get('content', ''), + 'kind': 'bug', + 'priority': self.request.unsafe_json_body['priority']['id'], + 'responsible': self.request.unsafe_json_body['responsible']['id'], + 'project': self.integration.config['project'] + } + try: + client = self.create_client() + issue = client.create_issue(form_data) + except IntegrationException as e: + self.request.response.status_code = 503 + return {'error_messages': [str(e)]} + + comment_body = 'Jira issue created: %s ' % issue['web_url'] + comment = ReportComment(user_name=self.request.user.user_name, + report_time=report.first_timestamp, + body=comment_body) + report.comments.append(comment) + return True + + @view_config(route_name='integrations_id', + match_param=['action=setup', 'integration=jira'], + renderer='json', + permission='edit') + def setup(self): + """ + Validates and creates integration between application and jira + """ + resource = self.request.context.resource + form = forms.IntegrationJiraForm( + MultiDict(self.request.safe_json_body or {}), + csrf_context=self.request, **self.integration_config) + if self.request.method == 'POST' and form.validate(): + integration_config = { + 'user_name': form.user_name.data, + 'password': form.password.data, + 'host_name': form.host_name.data, + 'project': form.project.data + } + if not self.integration: + # add new integration + self.integration = JiraIntegration( + modified_date=datetime.utcnow(), + ) + self.request.session.flash('Integration added') + resource.integrations.append(self.integration) + else: + self.request.session.flash('Integration updated') + self.integration.config = integration_config + return integration_config + elif self.request.method == 'POST': + return HTTPUnprocessableEntity(body=form.errors_json) + + to_return = copy.deepcopy(self.integration_config) + to_return.pop('password', None) + return to_return diff --git a/backend/src/appenlight/views/integrations/slack.py b/backend/src/appenlight/views/integrations/slack.py new file mode 100644 index 0000000..57c5dc6 --- /dev/null +++ b/backend/src/appenlight/views/integrations/slack.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import logging + +from appenlight.models import DBSession +from appenlight.models.integrations.slack import SlackIntegration, \ + IntegrationException +from pyramid.httpexceptions import HTTPUnprocessableEntity +from pyramid.view import view_config +from appenlight import forms +from datetime import datetime +from webob.multidict import MultiDict + +log = logging.getLogger(__name__) + +from . import IntegrationView + + +class SlackView(IntegrationView): + @view_config(route_name='integrations_id', + match_param=['action=info', 'integration=slack'], + renderer='json') + def get_info(self): + pass + + @view_config(route_name='integrations_id', + match_param=['action=setup', 'integration=slack'], + renderer='json', + permission='edit') + def setup(self): + """ + Validates and creates integration between application and slack + """ + resource = self.request.context.resource + self.create_missing_channel(resource, 'slack') + form = forms.IntegrationSlackForm( + MultiDict(self.request.safe_json_body or {}), + csrf_context=self.request, **self.integration_config) + + if self.request.method == 'POST' and form.validate(): + integration_config = { + 'webhook_url': form.webhook_url.data + } + if not self.integration: + # add new integration + self.integration = SlackIntegration( + modified_date=datetime.utcnow(), + ) + self.request.session.flash('Integration added') + resource.integrations.append(self.integration) + else: + self.request.session.flash('Integration updated') + self.integration.config = integration_config + DBSession.flush() + self.create_missing_channel(resource, 'slack') + return integration_config + elif self.request.method == 'POST': + return HTTPUnprocessableEntity(body=form.errors_json) + return self.integration_config diff --git a/backend/src/appenlight/views/integrations/webhooks.py b/backend/src/appenlight/views/integrations/webhooks.py new file mode 100644 index 0000000..86a740f --- /dev/null +++ b/backend/src/appenlight/views/integrations/webhooks.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +from appenlight.models import DBSession +from appenlight.models.integrations.webhooks import WebhooksIntegration, \ + IntegrationException +from pyramid.httpexceptions import HTTPUnprocessableEntity +from pyramid.view import view_config +from appenlight import forms +import logging +from datetime import datetime +from webob.multidict import MultiDict + +log = logging.getLogger(__name__) + +from . import IntegrationView + + +class WebhooksView(IntegrationView): + @view_config(route_name='integrations_id', + match_param=['action=info', 'integration=webhooks'], + renderer='json') + def get_info(self): + pass + + @view_config(route_name='integrations_id', + match_param=['action=setup', 'integration=webhooks'], + renderer='json', permission='edit') + def setup(self): + """ + Creates webhook integration + """ + resource = self.request.context.resource + self.create_missing_channel(resource, 'webhooks') + + form = forms.IntegrationWebhooksForm( + MultiDict(self.request.safe_json_body or {}), + csrf_context=self.request, **self.integration_config) + if self.request.method == 'POST' and form.validate(): + integration_config = { + 'reports_webhook': form.reports_webhook.data, + 'alerts_webhook': form.alerts_webhook.data, + } + if not self.integration: + # add new integration + self.integration = WebhooksIntegration( + modified_date=datetime.utcnow(), + ) + self.request.session.flash('Integration added') + resource.integrations.append(self.integration) + else: + self.request.session.flash('Integration updated') + self.integration.config = integration_config + DBSession.flush() + self.create_missing_channel(resource, 'webhooks') + return integration_config + elif self.request.method == 'POST': + return HTTPUnprocessableEntity(body=form.errors_json) + return self.integration_config diff --git a/backend/src/appenlight/views/logs.py b/backend/src/appenlight/views/logs.py new file mode 100644 index 0000000..45617c8 --- /dev/null +++ b/backend/src/appenlight/views/logs.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import logging +from datetime import datetime, timedelta + +from pyramid.view import view_config +from pyramid.httpexceptions import HTTPUnprocessableEntity +from appenlight.models import Datastores, Log +from appenlight.models.services.log import LogService +from appenlight.lib.utils import (build_filter_settings_from_query_dict, + es_index_name_limiter) +from appenlight.lib.helpers import gen_pagination_headers +from appenlight.celery.tasks import logs_cleanup + +log = logging.getLogger(__name__) + +section_filters_key = 'appenlight:logs:filter:%s' + + +@view_config(route_name='logs_no_id', renderer='json', + permission='authenticated') +def fetch_logs(request): + """ + Returns list of log entries from Elasticsearch + """ + + filter_settings = build_filter_settings_from_query_dict(request, + request.GET.mixed()) + logs_paginator = LogService.get_paginator_by_app_ids( + app_ids=filter_settings['resource'], + page=filter_settings['page'], + filter_settings=filter_settings + ) + headers = gen_pagination_headers(request, logs_paginator) + request.response.headers.update(headers) + + return [l.get_dict() for l in logs_paginator.sa_items] + + +@view_config(route_name='section_view', + match_param=['section=logs_section', 'view=fetch_series'], + renderer='json', permission='authenticated') +def logs_fetch_series(request): + """ + Handles metric dashboard graphs + Returns information for time/tier breakdown + """ + filter_settings = build_filter_settings_from_query_dict(request, + request.GET.mixed()) + paginator = LogService.get_paginator_by_app_ids( + app_ids=filter_settings['resource'], + page=1, filter_settings=filter_settings, items_per_page=1) + now = datetime.utcnow().replace(microsecond=0, second=0) + delta = timedelta(days=7) + if paginator.sa_items: + start_date = paginator.sa_items[-1].timestamp.replace(microsecond=0, + second=0) + filter_settings['start_date'] = start_date - delta + else: + filter_settings['start_date'] = now - delta + filter_settings['end_date'] = filter_settings['start_date'] \ + + timedelta(days=7) + since_when = filter_settings['start_date'] + + @request.registry.cache_regions.redis_sec_30.cache_on_arguments( + 'logs_graphs') + def cached(apps, search_params, delta, now): + data = LogService.get_time_series_aggregate( + filter_settings['resource'], filter_settings) + if not data: + return [] + buckets = data['aggregations']['events_over_time']['buckets'] + return [{"x": datetime.utcfromtimestamp(item["key"] / 1000), + "logs": item["doc_count"]} for item in buckets] + + return cached(filter_settings, request.GET.mixed(), delta, now) + + +@view_config(route_name='logs_no_id', renderer='json', request_method="DELETE", + permission='authenticated') +def logs_mass_delete(request): + params = request.GET.mixed() + if 'resource' not in params: + raise HTTPUnprocessableEntity() + # this might be '' and then colander will not validate the schema + if not params.get('namespace'): + params.pop('namespace', None) + filter_settings = build_filter_settings_from_query_dict( + request, params, resource_permissions=['update_reports']) + + resource_id = list(filter_settings['resource'])[0] + # filter settings returns list of all of users applications + # if app is not matching - normally we would not care as its used for search + # but here user playing with params would possibly wipe out their whole data + if int(resource_id) != int(params['resource']): + raise HTTPUnprocessableEntity() + + logs_cleanup.delay(resource_id, filter_settings) + msg = 'Log cleanup process started - it may take a while for ' \ + 'everything to get removed' + request.session.flash(msg) + return {} + + +@view_config(route_name='section_view', + match_param=("view=common_tags", "section=logs_section"), + renderer='json', permission='authenticated') +def common_tags(request): + config = request.GET.mixed() + filter_settings = build_filter_settings_from_query_dict(request, + config) + + resources = list(filter_settings["resource"]) + query = { + "query": { + "filtered": { + "filter": { + "and": [{"terms": {"resource_id": list(resources)}}] + } + } + } + } + start_date = filter_settings.get('start_date') + end_date = filter_settings.get('end_date') + filter_part = query['query']['filtered']['filter']['and'] + + date_range = {"range": {"timestamp": {}}} + if start_date: + date_range["range"]["timestamp"]["gte"] = start_date + if end_date: + date_range["range"]["timestamp"]["lte"] = end_date + if start_date or end_date: + filter_part.append(date_range) + + levels = filter_settings.get('level') + if levels: + filter_part.append({"terms": {'log_level': levels}}) + namespaces = filter_settings.get('namespace') + if namespaces: + filter_part.append({"terms": {'namespace': namespaces}}) + + query["aggs"] = { + "sub_agg": { + "terms": { + "field": "tag_list", + "size": 50 + } + } + } + # tags + index_names = es_index_name_limiter( + ixtypes=[config.get('datasource', 'logs')]) + result = Datastores.es.search(query, index=index_names, doc_type='log', + size=0) + tag_buckets = result['aggregations']['sub_agg'].get('buckets', []) + # namespaces + query["aggs"] = { + "sub_agg": { + "terms": { + "field": "namespace", + "size": 50 + } + } + } + result = Datastores.es.search(query, index=index_names, doc_type='log', + size=0) + namespaces_buckets = result['aggregations']['sub_agg'].get('buckets', []) + return { + "tags": [item['key'] for item in tag_buckets], + "namespaces": [item['key'] for item in namespaces_buckets] + } + + +@view_config(route_name='section_view', + match_param=("view=common_values", "section=logs_section"), + renderer='json', permission='authenticated') +def common_values(request): + config = request.GET.mixed() + datasource = config.pop('datasource', 'logs') + filter_settings = build_filter_settings_from_query_dict(request, + config) + resources = list(filter_settings["resource"]) + tag_name = filter_settings['tags'][0]['value'][0] + query = { + 'query': { + 'filtered': { + 'filter': { + 'and': [ + {'terms': {'resource_id': list(resources)}}, + {'terms': { + 'namespace': filter_settings['namespace']}} + ] + } + } + } + } + query['aggs'] = { + 'sub_agg': { + 'terms': { + 'field': 'tags.{}.values'.format(tag_name), + 'size': 50 + } + } + } + index_names = es_index_name_limiter(ixtypes=[datasource]) + result = Datastores.es.search(query, index=index_names, doc_type='log', + size=0) + values_buckets = result['aggregations']['sub_agg'].get('buckets', []) + return { + "values": [item['key'] for item in values_buckets] + } diff --git a/backend/src/appenlight/views/plugin_configs.py b/backend/src/appenlight/views/plugin_configs.py new file mode 100644 index 0000000..65bb659 --- /dev/null +++ b/backend/src/appenlight/views/plugin_configs.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +from pyramid.view import view_config +from appenlight.models.services.plugin_config import PluginConfigService + +import logging + +log = logging.getLogger(__name__) + + +@view_config(route_name='plugin_configs', renderer='json', + permission='edit', request_method='GET') +def query(request): + configs = PluginConfigService.by_query( + request.params.get('resource_id'), + plugin_name=request.matchdict.get('plugin_name'), + section=request.params.get('section')) + return [c for c in configs] diff --git a/backend/src/appenlight/views/reports.py b/backend/src/appenlight/views/reports.py new file mode 100644 index 0000000..d7f337f --- /dev/null +++ b/backend/src/appenlight/views/reports.py @@ -0,0 +1,299 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import logging + +from datetime import datetime, timedelta +from pyramid.view import view_config +from pyramid.httpexceptions import HTTPUnprocessableEntity + +from appenlight.models import DBSession +from appenlight.models.user import User +from appenlight.models.report_comment import ReportComment +from appenlight.models.report_assignment import ReportAssignment +from appenlight.models.services.user import UserService +from appenlight.models.services.report_group import ReportGroupService +from appenlight import forms +from appenlight.lib.enums import ReportType +from appenlight.lib.helpers import gen_pagination_headers +from appenlight.lib.utils import build_filter_settings_from_query_dict +from appenlight.validators import ReportSearchSchema, TagListSchema, \ + accepted_search_params +from webob.multidict import MultiDict + +_ = str + +log = logging.getLogger(__name__) + +section_filters_key = 'appenlight:reports:filter:%s' + + +@view_config(route_name='reports', renderer='json', permission='authenticated') +@view_config(route_name='slow_reports', renderer='json', + permission='authenticated') +def index(request): + """ + Returns list of report groups based on user search query + """ + if request.user: + request.user.last_login_date = datetime.utcnow() + + applications = request.user.resources_with_perms( + ['view'], resource_types=['application']) + + search_params = request.GET.mixed() + + all_possible_app_ids = set([app.resource_id for app in applications]) + schema = ReportSearchSchema().bind(resources=all_possible_app_ids) + tag_schema = TagListSchema() + filter_settings = schema.deserialize(search_params) + tag_list = [{"name": k, "value": v} for k, v in filter_settings.items() + if k not in accepted_search_params] + tags = tag_schema.deserialize(tag_list) + filter_settings['tags'] = tags + if request.matched_route.name == 'slow_reports': + filter_settings['report_type'] = [ReportType.slow] + else: + filter_settings['report_type'] = [ReportType.error] + + reports_paginator = ReportGroupService.get_paginator_by_app_ids( + app_ids=filter_settings['resource'], + page=filter_settings['page'], + filter_settings=filter_settings + ) + reports = [] + include_keys = ('id', 'http_status', 'report_type', 'resource_name', + 'front_url', 'resource_id', 'error', 'url_path', 'tags', + 'duration') + for report in reports_paginator.sa_items: + reports.append(report.get_dict(request, include_keys=include_keys)) + headers = gen_pagination_headers(request, reports_paginator) + request.response.headers.update(headers) + return reports + + +@view_config(route_name='report_groups', renderer='json', permission='view', + request_method="GET") +def view_report(request): + """ + Show individual detailed report group along with latest report + """ + report_group = request.context.report_group + if not report_group.read: + report_group.read = True + + report_id = request.params.get('reportId', request.params.get('report_id')) + report_dict = report_group.get_report(report_id).get_dict(request, + details=True) + # disallow browsing other occurences by anonymous + if not request.user: + report_dict.pop('group_next_report', None) + report_dict.pop('group_previous_report', None) + return report_dict + + +@view_config(route_name='report_groups', renderer='json', + permission='update_reports', request_method='DELETE') +def remove(request): + """ + Used to remove reourt groups from database + """ + report = request.context.report_group + form = forms.ReactorForm(request.POST, csrf_context=request) + form.validate() + DBSession.delete(report) + return True + + +@view_config(route_name='report_groups_property', match_param='key=comments', + renderer='json', permission='view', request_method="POST") +def comment_create(request): + """ + Creates user comments for report group, sends email notifications + of said comments + """ + report_group = request.context.report_group + application = request.context.resource + form = forms.CommentForm(MultiDict(request.unsafe_json_body), + csrf_context=request) + if request.method == 'POST' and form.validate(): + comment = ReportComment(owner_id=request.user.id, + report_time=report_group.first_timestamp) + form.populate_obj(comment) + report_group.comments.append(comment) + perm_list = application.users_for_perm('view') + uids_to_notify = [] + users_to_notify = [] + for perm in perm_list: + user = perm.user + if ('@{}'.format(user.user_name) in comment.body and + user.id not in uids_to_notify): + uids_to_notify.append(user.id) + users_to_notify.append(user) + + commenters = ReportGroupService.users_commenting( + report_group, exclude_user_id=request.user.id) + for user in commenters: + if user.id not in uids_to_notify: + uids_to_notify.append(user.id) + users_to_notify.append(user) + + for user in users_to_notify: + email_vars = {'user': user, + 'commenting_user': request.user, + 'request': request, + 'application': application, + 'report_group': report_group, + 'comment': comment, + 'email_title': "App Enlight :: New comment"} + UserService.send_email( + request, + recipients=[user.email], + variables=email_vars, + template='/email_templates/new_comment_report.jinja2') + request.session.flash(_('Your comment was created')) + return comment.get_dict() + else: + return form.errors + + +@view_config(route_name='report_groups_property', + match_param='key=assigned_users', renderer='json', + permission='update_reports', request_method="GET") +def assigned_users(request): + """ + Returns list of users a specific report group is assigned for review + """ + report_group = request.context.report_group + application = request.context.resource + users = set([p.user for p in application.users_for_perm('view')]) + currently_assigned = [u.user_name for u in report_group.assigned_users] + user_status = {'assigned': [], 'unassigned': []} + # handle users + for user in users: + user_dict = {'user_name': user.user_name, + 'gravatar_url': user.gravatar_url(), + 'name': '%s %s' % (user.first_name, user.last_name,)} + if user.user_name in currently_assigned: + user_status['assigned'].append(user_dict) + elif user_dict not in user_status['unassigned']: + user_status['unassigned'].append(user_dict) + return user_status + + +@view_config(route_name='report_groups_property', + match_param='key=assigned_users', renderer='json', + permission='update_reports', request_method="PATCH") +def assign_users(request): + """ + Assigns specific report group to user for review - send email notification + """ + report_group = request.context.report_group + application = request.context.resource + currently_assigned = [u.user_name for u in report_group.assigned_users] + new_assigns = request.unsafe_json_body + + # first unassign old users + for user_name in new_assigns['unassigned']: + if user_name in currently_assigned: + user = User.by_user_name(user_name) + report_group.assigned_users.remove(user) + comment = ReportComment(owner_id=request.user.id, + report_time=report_group.first_timestamp) + comment.body = 'Unassigned group from @%s' % user_name + report_group.comments.append(comment) + + # assign new users + for user_name in new_assigns['assigned']: + if user_name not in currently_assigned: + user = User.by_user_name(user_name) + if user in report_group.assigned_users: + report_group.assigned_users.remove(user) + DBSession.flush() + assignment = ReportAssignment( + owner_id=user.id, + report_time=report_group.first_timestamp, + group_id=report_group.id) + DBSession.add(assignment) + + comment = ReportComment(owner_id=request.user.id, + report_time=report_group.first_timestamp) + comment.body = 'Assigned report_group to @%s' % user_name + report_group.comments.append(comment) + + email_vars = {'user': user, + 'request': request, + 'application': application, + 'report_group': report_group, + 'email_title': "App Enlight :: Assigned Report"} + UserService.send_email(request, recipients=[user.email], + variables=email_vars, + template='/email_templates/assigned_report.jinja2') + + return True + + +@view_config(route_name='report_groups_property', match_param='key=history', + renderer='json', permission='view') +def history(request): + """ Separate error graph or similar graph""" + report_group = request.context.report_group + query_params = request.GET.mixed() + query_params['resource'] = (report_group.resource_id,) + + filter_settings = build_filter_settings_from_query_dict(request, + query_params) + if not filter_settings.get('end_date'): + end_date = datetime.utcnow().replace(microsecond=0, second=0) + filter_settings['end_date'] = end_date + + if not filter_settings.get('start_date'): + delta = timedelta(days=30) + filter_settings['start_date'] = filter_settings['end_date'] - delta + + filter_settings['group_id'] = report_group.id + + result = ReportGroupService.get_report_stats(request, filter_settings) + + plot_data = [] + for row in result: + point = { + 'x': row['x'], + 'reports': row['report'] + row['slow_report'] + row['not_found']} + plot_data.append(point) + + return plot_data + + +@view_config(route_name='report_groups', renderer='json', + permission='update_reports', request_method="PATCH") +def report_groups_PATCH(request): + """ + Used to update the report group fixed status + """ + report_group = request.context.report_group + allowed_keys = ['public', 'fixed'] + for k, v in request.unsafe_json_body.items(): + if k in allowed_keys: + setattr(report_group, k, v) + else: + return HTTPUnprocessableEntity() + return report_group.get_dict(request) diff --git a/backend/src/appenlight/views/tests.py b/backend/src/appenlight/views/tests.py new file mode 100644 index 0000000..118a55b --- /dev/null +++ b/backend/src/appenlight/views/tests.py @@ -0,0 +1,449 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import copy +import logging +import datetime +import time +import random +import redis +import six +import pyramid.renderers +import requests +import appenlight.celery.tasks +from pyramid.view import view_config +from pyramid_mailer.message import Message +from appenlight_client.timing import time_trace +from appenlight.models import DBSession, Datastores +from appenlight.models.user import User +from appenlight.models.report_group import ReportGroup +from appenlight.models.event import Event +from appenlight.models.services.report_group import ReportGroupService +from appenlight.models.services.event import EventService +from appenlight.lib.enums import ReportType + +log = logging.getLogger(__name__) + +GLOBAL_REQ = None + + +@view_config(route_name='test', match_param='action=mail', + renderer='string', permission='root_administration') +def mail(request): + """ + Test email communication + """ + request.environ['HTTP_HOST'] = 'appenlight.com' + request.environ['wsgi.url_scheme'] = 'https' + renderer_vars = {"title": "You have just registered on App Enlight", + "username": "test", + "email": "grzegżółka", + 'firstname': 'dupa'} + # return vars + html = pyramid.renderers.render('/email_templates/registered.jinja2', + renderer_vars, + request=request) + message = Message(subject="hello world %s" % random.randint(1, 9999), + sender="info@appenlight.com", + recipients=["ergo14@gmail.com"], + html=html) + request.registry.mailer.send(message) + return html + return vars + + +@view_config(route_name='test', match_param='action=alerting', + renderer='appenlight:templates/tests/alerting.jinja2', + permission='root_administration') +def alerting_test(request): + """ + Allows to test send data on various registered alerting channels + """ + applications = request.user.resources_with_perms( + ['view'], resource_types=['application']) + # what we can select in total + all_possible_app_ids = [app.resource_id for app in applications] + resource = applications[0] + + alert_channels = [] + for channel in request.user.alert_channels: + alert_channels.append(channel.get_dict()) + + cname = request.params.get('channel_name') + cvalue = request.params.get('channel_value') + event_name = request.params.get('event_name') + if cname and cvalue: + for channel in request.user.alert_channels: + if (channel.channel_value == cvalue and + channel.channel_name == cname): + break + if event_name in ['error_report_alert', 'slow_report_alert']: + # opened + new_event = Event(resource_id=resource.resource_id, + event_type=Event.types[event_name], + start_date=datetime.datetime.utcnow(), + status=Event.statuses['active'], + values={'reports': 5, + 'threshold': 10} + ) + channel.notify_alert(resource=resource, + event=new_event, + user=request.user, + request=request) + + # closed + ev_type = Event.types[event_name.replace('open', 'close')] + new_event = Event(resource_id=resource.resource_id, + event_type=ev_type, + start_date=datetime.datetime.utcnow(), + status=Event.statuses['closed'], + values={'reports': 5, + 'threshold': 10}) + channel.notify_alert(resource=resource, + event=new_event, + user=request.user, + request=request) + elif event_name == 'notify_reports': + report = ReportGroupService.by_app_ids(all_possible_app_ids) \ + .filter(ReportGroup.report_type == ReportType.error).first() + confirmed_reports = [(5, report), (1, report)] + channel.notify_reports(resource=resource, + user=request.user, + request=request, + since_when=datetime.datetime.utcnow(), + reports=confirmed_reports) + confirmed_reports = [(5, report)] + channel.notify_reports(resource=resource, + user=request.user, + request=request, + since_when=datetime.datetime.utcnow(), + reports=confirmed_reports) + elif event_name == 'notify_uptime': + new_event = Event(resource_id=resource.resource_id, + event_type=Event.types['uptime_alert'], + start_date=datetime.datetime.utcnow(), + status=Event.statuses['active'], + values={"status_code": 500, + "tries": 2, + "response_time": 0}) + channel.notify_uptime_alert(resource=resource, + event=new_event, + user=request.user, + request=request) + elif event_name == 'chart_alert': + event = EventService.by_type_and_status( + event_types=(Event.types['chart_alert'],), + status_types=(Event.statuses['active'],)).first() + channel.notify_chart_alert(resource=event.resource, + event=event, + user=request.user, + request=request) + elif event_name == 'daily_digest': + since_when = datetime.datetime.utcnow() - datetime.timedelta( + hours=8) + filter_settings = {'resource': [resource.resource_id], + 'tags': [{'name': 'type', + 'value': ['error'], 'op': None}], + 'type': 'error', 'start_date': since_when} + + reports = ReportGroupService.get_trending( + request, filter_settings=filter_settings, limit=50) + channel.send_digest(resource=resource, + user=request.user, + request=request, + since_when=datetime.datetime.utcnow(), + reports=reports) + + return {'alert_channels': alert_channels, + 'applications': dict([(app.resource_id, app.resource_name) + for app in applications.all()])} + + +@view_config(route_name='test', match_param='action=error', + renderer='string', permission='root_administration') +def error(request): + """ + Raises an internal error with some test data for testing purposes + """ + request.environ['appenlight.message'] = 'test message' + request.environ['appenlight.extra']['dupa'] = 'dupa' + request.environ['appenlight.extra']['message'] = 'message' + request.environ['appenlight.tags']['action'] = 'test_error' + request.environ['appenlight.tags']['count'] = 5 + log.debug(chr(960)) + log.debug('debug') + log.info(chr(960)) + log.info('INFO') + log.warning('warning') + + @time_trace(name='error.foobar', min_duration=0.1) + def fooobar(): + time.sleep(0.12) + return 1 + + fooobar() + + def foobar(somearg): + raise Exception('test') + + client = redis.StrictRedis() + client.setex('testval', 10, 'foo') + request.environ['appenlight.force_send'] = 1 + + # stats, result = get_local_storage(local_timing).get_thread_stats() + # import pprint + # pprint.pprint(stats) + # pprint.pprint(result) + # print 'entries', len(result) + request.environ['appenlight.username'] = 'ErgO' + raise Exception(chr(960) + '%s' % random.randint(1, 5)) + return {} + + +@view_config(route_name='test', match_param='action=task', + renderer='string', permission='root_administration') +def test_task(request): + """ + Test erroneous celery task + """ + import appenlight.celery.tasks + + appenlight.celery.tasks.test_exception_task.delay() + return 'task sent' + + +@view_config(route_name='test', match_param='action=task_retry', + renderer='string', permission='root_administration') +def test_task_retry(request): + """ + Test erroneous celery task + """ + import appenlight.celery.tasks + + appenlight.celery.tasks.test_retry_exception_task.delay() + return 'task sent' + + +@view_config(route_name='test', match_param='action=celery_emails', + renderer='string', permission='root_administration') +def test_celery_emails(request): + import appenlight.celery.tasks + appenlight.celery.tasks.alerting.delay() + return 'task sent' + + +@view_config(route_name='test', match_param='action=daily_digest', + renderer='string', permission='root_administration') +def test_celery_daily_digest(request): + import appenlight.celery.tasks + appenlight.celery.tasks.daily_digest.delay() + return 'task sent' + + +@view_config(route_name='test', match_param='action=celery_alerting', + renderer='string', permission='root_administration') +def test_celery_alerting(request): + import appenlight.celery.tasks + appenlight.celery.tasks.alerting() + return 'task sent' + + +@view_config(route_name='test', match_param='action=logging', + renderer='string', permission='root_administration') +def logs(request): + """ + Test some in-app logging + """ + log.debug(chr(960)) + log.debug('debug') + log.info(chr(960)) + log.info('INFO') + log.warning('Matched GET /\xc4\x85\xc5\xbc\xc4\x87' + '\xc4\x99\xc4\x99\xc4\x85/summary') + log.warning('XXXXMatched GET /\xc4\x85\xc5\xbc\xc4' + '\x87\xc4\x99\xc4\x99\xc4\x85/summary') + log.warning('DUPA /ążćęęą') + log.warning("g\u017ceg\u017c\u00f3\u0142ka") + log.error('TEST Lorem ipsum2', + extra={'user': 'ergo', 'commit': 'sog8ds0g7sdih12hh1j512h5k'}) + log.fatal('TEST Lorem ipsum3') + log.warning('TEST Lorem ipsum', + extra={"action": 'purchase', + "price": random.random() * 100, + "quantity": random.randint(1, 99)}) + log.warning('test_pkey', + extra={"action": 'test_pkey', "price": random.random() * 100, + 'ae_primary_key': 1, + "quantity": random.randint(1, 99)}) + log.warning('test_pkey2', + extra={"action": 'test_pkey', "price": random.random() * 100, + 'ae_primary_key': 'b', + 'ae_permanent': 't', + "quantity": random.randint(1, 99)}) + log.warning('test_pkey3', + extra={"action": 'test_pkey', "price": random.random() * 100, + 'ae_primary_key': 1, + "quantity": random.randint(1, 99)}) + log.warning('test_pkey4', + extra={"action": 'test_pkey', "price": random.random() * 100, + 'ae_primary_key': 'b', + 'ae_permanent': True, + "quantity": random.randint(1, 99)}) + request.environ['appenlight.force_send'] = 1 + return {} + + +@view_config(route_name='test', match_param='action=transaction', + renderer='string', permission='root_administration') +def transaction_test(request): + """ + Test transactions + """ + try: + result = DBSession.execute("SELECT 1/0") + except: + request.tm.abort() + result = DBSession.execute("SELECT 1") + return 'OK' + + +@view_config(route_name='test', match_param='action=slow_request', + renderer='string', permission='root_administration') +def slow_request(request): + """ + Test a request that has some slow entries - including nested calls + """ + users = DBSession.query(User).all() + import random + some_val = random.random() + import threading + t_id = id(threading.currentThread()) + log.warning('slow_log %s %s ' % (some_val, t_id)) + log.critical('tid %s' % t_id) + + @time_trace(name='baz_func %s' % some_val, min_duration=0.1) + def baz(arg): + time.sleep(0.32) + return arg + + requests.get('http://ubuntu.com') + + @time_trace(name='foo_func %s %s' % (some_val, t_id), min_duration=0.1) + def foo(arg): + time.sleep(0.52) + log.warning('foo_func %s %s' % (some_val, t_id)) + requests.get('http://ubuntu.com?test=%s' % some_val) + return bar(arg) + + @time_trace(name='bar_func %s %s' % (some_val, t_id), min_duration=0.1) + def bar(arg): + log.warning('bar_func %s %s' % (some_val, t_id)) + time.sleep(1.52) + baz(arg) + baz(arg) + return baz(arg) + + foo('a') + return {} + + +@view_config(route_name='test', match_param='action=styling', + renderer='appenlight:templates/tests/styling.jinja2', + permission='__no_permission_required__') +def styling(request): + """ + Some styling test page + """ + _ = str + request.session.flash(_( + 'Your password got updated. ' + 'Next time log in with your new credentials.')) + request.session.flash(_( + 'Something went wrong when we ' + 'tried to authorize you via external provider'), + 'warning') + request.session.flash(_( + 'Unfortunately there was a problem ' + 'processing your payment, please try again later.'), + 'error') + return {} + + +@view_config(route_name='test', match_param='action=js_error', + renderer='appenlight:templates/tests/js_error.jinja2', + permission='__no_permission_required__') +def js(request): + """ + Used for testing javasctipt client for error catching + """ + return {} + + +@view_config(route_name='test', match_param='action=js_log', + renderer='appenlight:templates/tests/js_log.jinja2', + permission='__no_permission_required__') +def js_log(request): + """ + Used for testing javasctipt client for logging + """ + return {} + + +@view_config(route_name='test', match_param='action=log_requests', + renderer='string', + permission='__no_permission_required__') +def log_requests(request): + """ + Util view for printing json requests + """ + return {} + + +@view_config(route_name='test', match_param='action=url', renderer='string', + permission='__no_permission_required__') +def log_requests(request): + """ + I have no fucking clue why I needed that ;-) + """ + return request.route_url('reports', _app_url='https://appenlight.com') + + +class TestClass(object): + """ + Used to test if class-based view name resolution works correctly + """ + + def __init__(self, request): + self.request = request + + @view_config(route_name='test', match_param='action=test_a', + renderer='string', permission='root_administration') + @view_config(route_name='test', match_param='action=test_c', + renderer='string', permission='root_administration') + @view_config(route_name='test', match_param='action=test_d', + renderer='string', permission='root_administration') + def test_a(self): + return 'ok' + + @view_config(route_name='test', match_param='action=test_b', + renderer='string', permission='root_administration') + def test_b(self): + return 'ok' diff --git a/backend/src/appenlight/views/user.py b/backend/src/appenlight/views/user.py new file mode 100644 index 0000000..f940ad9 --- /dev/null +++ b/backend/src/appenlight/views/user.py @@ -0,0 +1,678 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# App Enlight Enterprise Edition, including its added features, Support +# services, and proprietary license terms, please see +# https://rhodecode.com/licenses/ + +import colander +import datetime +import json +import logging +import uuid +import pyramid.security as security +import appenlight.lib.helpers as h + +from authomatic.adapters import WebObAdapter +from pyramid.view import view_config +from pyramid.httpexceptions import HTTPFound, HTTPUnprocessableEntity +from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest +from pyramid.security import NO_PERMISSION_REQUIRED +from ziggurat_foundations.models.services.external_identity import \ + ExternalIdentityService + +from appenlight.lib import generate_random_string +from appenlight.lib.social import handle_social_data +from appenlight.lib.utils import cometd_request, add_cors_headers, \ + permission_tuple_to_dict +from appenlight.models import DBSession +from appenlight.models.alert_channels.email import EmailAlertChannel +from appenlight.models.alert_channel_action import AlertChannelAction +from appenlight.models.services.alert_channel import AlertChannelService +from appenlight.models.services.alert_channel_action import \ + AlertChannelActionService +from appenlight.models.auth_token import AuthToken +from appenlight.models.report import REPORT_TYPE_MATRIX +from appenlight.models.user import User +from appenlight.models.services.user import UserService +from appenlight.subscribers import _ +from appenlight.validators import build_rule_schema +from appenlight import forms +from webob.multidict import MultiDict + +log = logging.getLogger(__name__) + + +@view_config(route_name='users_no_id', renderer='json', + request_method="GET", permission='root_administration') +def users_list(request): + """ + Returns users list + """ + props = ['user_name', 'id', 'first_name', 'last_name', 'email', + 'last_login_date', 'status'] + users = UserService.all() + users_dicts = [] + for user in users: + u_dict = user.get_dict(include_keys=props) + u_dict['gravatar_url'] = user.gravatar_url(s=20) + users_dicts.append(u_dict) + return users_dicts + + +@view_config(route_name='users_no_id', renderer='json', + request_method="POST", permission='root_administration') +def users_create(request): + """ + Returns users list + """ + form = forms.UserCreateForm(MultiDict(request.safe_json_body or {}), + csrf_context=request) + if form.validate(): + log.info('registering user') + user = User() + # insert new user here + DBSession.add(user) + form.populate_obj(user) + user.regenerate_security_code() + user.set_password(user.user_password) + user.status = 1 if form.status.data else 0 + request.session.flash(_('User created')) + DBSession.flush() + return user.get_dict(exclude_keys=['security_code_date', 'notes', + 'security_code', 'user_password']) + else: + return HTTPUnprocessableEntity(body=form.errors_json) + + +@view_config(route_name='users', renderer='json', + request_method="GET", permission='root_administration') +@view_config(route_name='users', renderer='json', + request_method="PATCH", permission='root_administration') +def users_update(request): + """ + Updates user object + """ + user = User.by_id(request.matchdict.get('user_id')) + if not user: + return HTTPNotFound() + post_data = request.safe_json_body or {} + if request.method == 'PATCH': + form = forms.UserUpdateForm(MultiDict(post_data), + csrf_context=request) + if form.validate(): + form.populate_obj(user, ignore_none=True) + if form.user_password.data: + user.set_password(user.user_password) + if form.status.data: + user.status = 1 + else: + user.status = 0 + else: + return HTTPUnprocessableEntity(body=form.errors_json) + return user.get_dict(exclude_keys=['security_code_date', 'notes', + 'security_code', 'user_password']) + + +@view_config(route_name='users_property', + match_param='key=resource_permissions', + renderer='json', permission='authenticated') +def users_resource_permissions_list(request): + """ + Get list of permissions assigned to specific resources + """ + user = User.by_id(request.matchdict.get('user_id')) + if not user: + return HTTPNotFound() + return [permission_tuple_to_dict(perm) for perm in + user.resources_with_possible_perms()] + + +@view_config(route_name='users', renderer='json', + request_method="DELETE", permission='root_administration') +def users_DELETE(request): + """ + Removes a user permanently from db - makes a check to see if after the + operation there will be at least one admin left + """ + msg = _('There needs to be at least one administrator in the system') + user = User.by_id(request.matchdict.get('user_id')) + if user: + users = User.users_for_perms(['root_administration']).all() + if len(users) < 2 and user.id == users[0].id: + request.session.flash(msg, 'warning') + else: + DBSession.delete(user) + request.session.flash(_('User removed')) + return True + request.response.status = 422 + return False + + +@view_config(route_name='users_self', renderer='json', + request_method="GET", permission='authenticated') +@view_config(route_name='users_self', renderer='json', + request_method="PATCH", permission='authenticated') +def users_self(request): + """ + Updates user personal information + """ + + if request.method == 'PATCH': + form = forms.gen_user_profile_form()( + MultiDict(request.unsafe_json_body), + csrf_context=request) + if form.validate(): + form.populate_obj(request.user) + request.session.flash(_('Your profile got updated.')) + else: + return HTTPUnprocessableEntity(body=form.errors_json) + return request.user.get_dict(exclude_keys=['security_code_date', 'notes', + 'security_code', + 'user_password']) + + +@view_config(route_name='users_self_property', + match_param='key=external_identities', renderer='json', + request_method='GET', permission='authenticated') +def users_external_identies(request): + user = request.user + identities = [{'provider': ident.provider_name, + 'id': ident.external_user_name} for ident + in user.external_identities.all()] + return identities + + +@view_config(route_name='users_self_property', + match_param='key=external_identities', renderer='json', + request_method='DELETE', permission='authenticated') +def users_external_identies_DELETE(request): + """ + Unbinds external identities(google,twitter etc.) from user account + """ + user = request.user + for identity in user.external_identities.all(): + log.info('found identity %s' % identity) + if (identity.provider_name == request.params.get('provider') and + identity.external_user_name == request.params.get('id')): + log.info('remove identity %s' % identity) + DBSession.delete(identity) + return True + return False + + +@view_config(route_name='users_self_property', + match_param='key=password', renderer='json', + request_method='PATCH', permission='authenticated') +def users_password(request): + """ + Sets new password for user account + """ + user = request.user + form = forms.ChangePasswordForm(MultiDict(request.unsafe_json_body), + csrf_context=request) + form.old_password.user = user + if form.validate(): + user.regenerate_security_code() + user.set_password(form.new_password.data) + msg = 'Your password got updated. ' \ + 'Next time log in with your new credentials.' + request.session.flash(_(msg)) + return True + else: + return HTTPUnprocessableEntity(body=form.errors_json) + return False + + +@view_config(route_name='users_self_property', match_param='key=websocket', + renderer='json', permission='authenticated') +def users_websocket(request): + """ + Handle authorization of users trying to connect + """ + # handle preflight request + user = request.user + if request.method == 'OPTIONS': + res = request.response.body('OK') + add_cors_headers(res) + return res + applications = user.resources_with_perms( + ['view'], resource_types=['application']) + channels = ['app_%s' % app.resource_id for app in applications] + payload = {"username": user.user_name, + "conn_id": str(uuid.uuid4()), + "channels": channels + } + settings = request.registry.settings + response = cometd_request( + settings['cometd.secret'], '/connect', payload, + servers=[request.registry.settings['cometd_servers']], + throw_exceptions=True) + return payload + + +@view_config(route_name='users_self_property', request_method="GET", + match_param='key=alert_channels', renderer='json', + permission='authenticated') +def alert_channels(request): + """ + Lists all available alert channels + """ + user = request.user + return [c.get_dict(extended_info=True) for c in user.alert_channels] + + +@view_config(route_name='users_self_property', match_param='key=alert_actions', + request_method="GET", renderer='json', permission='authenticated') +def alert_actions(request): + """ + Lists all available alert channels + """ + user = request.user + return [r.get_dict(extended_info=True) for r in user.alert_actions] + + +@view_config(route_name='users_self_property', renderer='json', + match_param='key=alert_channels_rules', request_method='POST', + permission='authenticated') +def alert_channels_rule_POST(request): + """ + Creates new notification rule for specific alert channel + """ + user = request.user + alert_action = AlertChannelAction(owner_id=request.user.id, + type='report') + DBSession.add(alert_action) + DBSession.flush() + return alert_action.get_dict() + + +@view_config(route_name='users_self_property', permission='authenticated', + match_param='key=alert_channels_rules', + renderer='json', request_method='DELETE') +def alert_channels_rule_DELETE(request): + """ + Removes specific alert channel rule + """ + user = request.user + rule_action = AlertChannelActionService.by_owner_id_and_pkey( + user.id, + request.GET.get('pkey')) + if rule_action: + DBSession.delete(rule_action) + return True + return HTTPNotFound() + + +@view_config(route_name='users_self_property', permission='authenticated', + match_param='key=alert_channels_rules', + renderer='json', request_method='PATCH') +def alert_channels_rule_PATCH(request): + """ + Removes specific alert channel rule + """ + user = request.user + json_body = request.unsafe_json_body + + schema = build_rule_schema(json_body['rule'], REPORT_TYPE_MATRIX) + try: + schema.deserialize(json_body['rule']) + except colander.Invalid as exc: + return HTTPUnprocessableEntity(body=json.dumps(exc.asdict())) + + rule_action = AlertChannelActionService.by_owner_id_and_pkey( + user.id, + request.GET.get('pkey')) + + if rule_action: + rule_action.rule = json_body['rule'] + rule_action.resource_id = json_body['resource_id'] + rule_action.action = json_body['action'] + return rule_action.get_dict() + return HTTPNotFound() + + +@view_config(route_name='users_self_property', permission='authenticated', + match_param='key=alert_channels', + renderer='json', request_method='PATCH') +def alert_channels_PATCH(request): + user = request.user + channel_name = request.GET.get('channel_name') + channel_value = request.GET.get('channel_value') + # iterate over channels + channel = None + for channel in user.alert_channels: + if (channel.channel_name == channel_name and + channel.channel_value == channel_value): + break + if not channel: + return HTTPNotFound() + + allowed_keys = ['daily_digest', 'send_alerts'] + for k, v in request.unsafe_json_body.items(): + if k in allowed_keys: + setattr(channel, k, v) + else: + return HTTPBadRequest() + return channel.get_dict() + + +@view_config(route_name='users_self_property', permission='authenticated', + match_param='key=alert_channels', + request_method="POST", renderer='json') +def alert_channels_POST(request): + """ + Creates a new email alert channel for user, sends a validation email + """ + user = request.user + form = forms.EmailChannelCreateForm(MultiDict(request.unsafe_json_body), + csrf_context=request) + if not form.validate(): + return HTTPUnprocessableEntity(body=form.errors_json) + + email = form.email.data.strip() + channel = EmailAlertChannel() + channel.channel_name = 'email' + channel.channel_value = email + security_code = generate_random_string(10) + channel.channel_json_conf = {'security_code': security_code} + user.alert_channels.append(channel) + + email_vars = {'user': user, + 'email': email, + 'request': request, + 'security_code': security_code, + 'email_title': "App Enlight :: " + "Please authorize your email"} + + UserService.send_email(request, recipients=[email], + variables=email_vars, + template='/email_templates/authorize_email.jinja2') + request.session.flash(_('Your alert channel was ' + 'added to the system.')) + request.session.flash( + _('You need to authorize your email channel, a message was ' + 'sent containing necessary information.'), + 'warning') + DBSession.flush() + channel.get_dict() + + +@view_config(route_name='section_view', + match_param=['section=user_section', + 'view=alert_channels_authorize'], + renderer='string', permission='authenticated') +def alert_channels_authorize(request): + """ + Performs alert channel authorization based on auth code sent in email + """ + user = request.user + for channel in user.alert_channels: + security_code = request.params.get('security_code', '') + if channel.channel_json_conf['security_code'] == security_code: + channel.channel_validated = True + request.session.flash(_('Your email was authorized.')) + return HTTPFound(location=request.route_url('/')) + + +@view_config(route_name='users_self_property', request_method="DELETE", + match_param='key=alert_channels', renderer='json', + permission='authenticated') +def alert_channel_DELETE(request): + """ + Removes alert channel from users channel + """ + user = request.user + channel = None + for chan in user.alert_channels: + if (chan.channel_name == request.params.get('channel_name') and + chan.channel_value == request.params.get('channel_value')): + channel = chan + break + if channel: + user.alert_channels.remove(channel) + request.session.flash(_('Your channel was removed.')) + return True + return False + + +@view_config(route_name='users_self_property', permission='authenticated', + match_param='key=alert_channels_actions_binds', + renderer='json', request_method="POST") +def alert_channels_actions_binds_POST(request): + """ + Adds alert action to users channels + """ + user = request.user + json_body = request.unsafe_json_body + channel = AlertChannelService.by_owner_id_and_pkey( + user.id, + json_body.get('channel_pkey')) + + rule_action = AlertChannelActionService.by_owner_id_and_pkey( + user.id, + json_body.get('action_pkey')) + + if channel and rule_action: + if channel.pkey not in [c.pkey for c in rule_action.channels]: + rule_action.channels.append(channel) + return rule_action.get_dict(extended_info=True) + return HTTPUnprocessableEntity() + + +@view_config(route_name='users_self_property', request_method="DELETE", + match_param='key=alert_channels_actions_binds', + renderer='json', permission='authenticated') +def alert_channels_actions_binds_DELETE(request): + """ + Removes alert action from users channels + """ + user = request.user + channel = AlertChannelService.by_owner_id_and_pkey( + user.id, + request.GET.get('channel_pkey')) + + rule_action = AlertChannelActionService.by_owner_id_and_pkey( + user.id, + request.GET.get('action_pkey')) + + if channel and rule_action: + if channel.pkey in [c.pkey for c in rule_action.channels]: + rule_action.channels.remove(channel) + return rule_action.get_dict(extended_info=True) + return HTTPUnprocessableEntity() + + +@view_config(route_name='social_auth_abort', + renderer='string', permission=NO_PERMISSION_REQUIRED) +def oauth_abort(request): + """ + Handles problems with authorization via velruse + """ + + +@view_config(route_name='social_auth', permission=NO_PERMISSION_REQUIRED) +def social_auth(request): + # Get the internal provider name URL variable. + provider_name = request.matchdict.get('provider') + + # Start the login procedure. + adapter = WebObAdapter(request, request.response) + result = request.registry.authomatic.login(adapter, provider_name) + if result: + if result.error: + return handle_auth_error(request, result) + elif result.user: + return handle_auth_success(request, result) + return request.response + + +def handle_auth_error(request, result): + # Login procedure finished with an error. + request.session.pop('zigg.social_auth', None) + request.session.flash(_('Something went wrong when we tried to ' + 'authorize you via external provider. ' + 'Please try again.'), 'warning') + + return HTTPFound(location=request.route_url('/')) + + +def handle_auth_success(request, result): + # Hooray, we have the user! + # OAuth 2.0 and OAuth 1.0a provide only limited user data on login, + # We need to update the user to get more info. + if result.user: + result.user.update() + + social_data = { + 'user': {'data': result.user.data}, + 'credentials': result.user.credentials + } + # normalize data + social_data['user']['id'] = result.user.id + user_name = result.user.username or '' + # use email name as username for google + if (social_data['credentials'].provider_name == 'google' and + result.user.email): + user_name = result.user.email + social_data['user']['user_name'] = user_name + social_data['user']['email'] = result.user.email or '' + + request.session['zigg.social_auth'] = social_data + # user is logged so bind his external identity with account + if request.user: + handle_social_data(request, request.user, social_data) + request.session.pop('zigg.social_auth', None) + return HTTPFound(location=request.route_url('/')) + else: + user = ExternalIdentityService.user_by_external_id_and_provider( + social_data['user']['id'], + social_data['credentials'].provider_name + ) + # fix legacy accounts with wrong google ID + if not user and social_data['credentials'].provider_name == 'google': + user = ExternalIdentityService.user_by_external_id_and_provider( + social_data['user']['email'], + social_data['credentials'].provider_name) + + # user tokens are already found in our db + if user: + handle_social_data(request, user, social_data) + headers = security.remember(request, user.id) + request.session.pop('zigg.social_auth', None) + return HTTPFound(location=request.route_url('/'), headers=headers) + else: + msg = 'You need to finish registration ' \ + 'process to bind your external identity to your account ' \ + 'or sign in to existing account' + request.session.flash(msg) + return HTTPFound(location=request.route_url('register')) + + +@view_config(route_name='section_view', permission='authenticated', + match_param=['section=users_section', 'view=search_users'], + renderer='json') +def search_users(request): + """ + Returns a list of users for autocomplete + """ + user = request.user + items_returned = [] + like_condition = request.params.get('user_name', '') + '%' + # first append used if email is passed + found_user = User.by_email(request.params.get('user_name', '')) + if found_user: + name = '{} {}'.format(found_user.first_name, found_user.last_name) + items_returned.append({'user': found_user.user_name, 'name': name}) + for found_user in User.user_names_like(like_condition).limit(20): + name = '{} {}'.format(found_user.first_name, found_user.last_name) + items_returned.append({'user': found_user.user_name, 'name': name}) + return items_returned + + +@view_config(route_name='users_self_property', match_param='key=auth_tokens', + request_method="GET", renderer='json', permission='authenticated') +@view_config(route_name='users_property', match_param='key=auth_tokens', + request_method="GET", renderer='json', permission='authenticated') +def auth_tokens_list(request): + """ + Lists all available alert channels + """ + if request.matched_route.name == 'users_self_property': + user = request.user + else: + user = User.by_id(request.matchdict.get('user_id')) + if not user: + return HTTPNotFound() + return [c.get_dict() for c in user.auth_tokens] + + +@view_config(route_name='users_self_property', match_param='key=auth_tokens', + request_method="POST", renderer='json', + permission='authenticated') +@view_config(route_name='users_property', match_param='key=auth_tokens', + request_method="POST", renderer='json', + permission='authenticated') +def auth_tokens_POST(request): + """ + Lists all available alert channels + """ + if request.matched_route.name == 'users_self_property': + user = request.user + else: + user = User.by_id(request.matchdict.get('user_id')) + if not user: + return HTTPNotFound() + + req_data = request.safe_json_body or {} + if not req_data.get('expires'): + req_data.pop('expires', None) + form = forms.AuthTokenCreateForm(MultiDict(req_data), csrf_context=request) + if not form.validate(): + return HTTPUnprocessableEntity(body=form.errors_json) + token = AuthToken() + form.populate_obj(token) + if token.expires: + interval = h.time_deltas.get(token.expires)['delta'] + token.expires = datetime.datetime.utcnow() + interval + user.auth_tokens.append(token) + DBSession.flush() + return token.get_dict() + + +@view_config(route_name='users_self_property', match_param='key=auth_tokens', + request_method="DELETE", renderer='json', + permission='authenticated') +@view_config(route_name='users_property', match_param='key=auth_tokens', + request_method="DELETE", renderer='json', + permission='authenticated') +def auth_tokens_DELETE(request): + """ + Lists all available alert channels + """ + if request.matched_route.name == 'users_self_property': + user = request.user + else: + user = User.by_id(request.matchdict.get('user_id')) + if not user: + return HTTPNotFound() + + for token in user.auth_tokens: + if token.token == request.params.get('token'): + user.auth_tokens.remove(token) + return True + return False diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..f5103d8 --- /dev/null +++ b/default.nix @@ -0,0 +1,362 @@ +{ system ? builtins.currentSystem +}: +let +pkgs = import { inherit system; }; +inherit (pkgs) fetchurl fetchgit; +buildPythonPackage = pkgs.python27Packages.buildPythonPackage; +python = pkgs.python27Packages.python; + + + +AnyKeystore = buildPythonPackage rec { + name = "anykeystore-0.2"; + propagatedBuildInputs = [ + pkgs.python27Packages.unittest2 + pkgs.python27Packages.mock + ]; + src = fetchurl { + url = "https://pypi.python.org/packages/source/a/anykeystore/${name}.tar.gz"; + md5 = "cc331cc5aa6eea38e498cd9cd73e8241"; + }; +}; + + +Ordereddict = buildPythonPackage rec { + name = "ordereddict-1.1"; + src = fetchurl { + url = "https://pypi.python.org/packages/source/o/ordereddict/${name}.tar.gz"; + md5 = "a0ed854ee442051b249bfad0f638bbec"; + }; +}; + + +HTTPPretty = buildPythonPackage rec { + name = "httpretty-0.8.10"; + propagatedBuildInputs = [ + pkgs.python27Packages.sure + ]; + src = fetchurl { + url = "https://pypi.python.org/packages/source/h/httpretty/${name}.tar.gz"; + md5 = "9c130b16726cbf85159574ae5761bce7"; + }; + doCheck = false; # error: Could not find suitable distribution for Requirement.parse('sure==1.2.3') +}; + + +Redis = buildPythonPackage rec { + name = "redis-2.10.3"; + propagatedBuildInputs = [ + pkgs.python27Packages.pytest + ]; + src = fetchurl { + url = "https://pypi.python.org/packages/source/r/redis/${name}.tar.gz"; + md5 = "7619221ad0cbd124a5687458ea3f5289"; + }; +}; + + +PyTestInstaFail = buildPythonPackage rec { + name = "pytest-instafail-0.3.0"; + propagatedBuildInputs = [ + pkgs.python27Packages.pytest + ]; + src = fetchurl { + url = "https://pypi.python.org/packages/source/p/pytest-instafail/${name}.tar.gz"; + md5 = "561e8c70038ae07404bdd94b0e0318d6"; + }; +}; + + +PythonOpenId = buildPythonPackage rec { + name = "python-openid-2.2.5"; + propagatedBuildInputs = [ + pkgs.python27Packages.pytest + ]; + src = fetchurl { + url = "https://pypi.python.org/packages/source/p/python-openid/${name}.tar.gz"; + md5 = "393f48b162ec29c3de9e2973548ea50d"; + }; +}; + + +################# DEPS OF DEPS END ####################### + +AppEnlightClient = buildPythonPackage rec { + name = "appenlight_client-0.6.14"; + propagatedBuildInputs = [ + pkgs.python27Packages.requests2 + pkgs.python27Packages.webob + ]; + src = fetchurl { + url = "https://pypi.python.org/packages/source/a/appenlight-client/${name}.tar.gz"; + md5 = "578c69b09f4356d898fff1199b98a95c"; + }; +}; + + +PyCountry = buildPythonPackage rec { + name = "pycountry-1.14"; + src = fetchurl { + url = "https://pypi.python.org/packages/source/p/pycountry/${name}.tar.gz"; + md5 = "f601972df38b39f02247e218e81ecf71"; + }; +}; + + +Camplight = buildPythonPackage rec { + name = "camplight-0.9.6"; + buildInputs = [ + pkgs.python27Packages.pytest + pkgs.python27Packages.requests + pkgs.python27Packages.sure + HTTPPretty + ]; + src = fetchurl { + url = "https://pypi.python.org/packages/source/c/camplight/${name}.tar.gz"; + md5 = "716cc7a4ea30da34ae4fcbfe2784ce59"; + }; +}; + + +DefusedXML = buildPythonPackage rec { + name = "defusedxml-0.4.1"; + src = fetchurl { + url = "https://pypi.python.org/packages/source/d/defusedxml/${name}.tar.gz"; + md5 = "230a5eff64f878b392478e30376d673a"; + }; +}; + + +FormEncode = buildPythonPackage rec { + name = "FormEncode-1.3.0"; + propagatedBuildInputs = [ + pkgs.python27Packages.dns + pkgs.python27Packages.nose + PyCountry + ]; + src = fetchurl { + url = "https://pypi.python.org/packages/source/F/FormEncode/${name}.zip"; + md5 = "6df12d60bf3179402f2c2efd1129eb74"; + }; +}; + + +JIRA = buildPythonPackage rec { + name = "jira-0.50"; + propagatedBuildInputs = [ + pkgs.python27Packages.autopep8 + pkgs.python27Packages.covCore + pkgs.python27Packages.pytest + pkgs.python27Packages.pytest_xdist + pkgs.python27Packages.pytestpep8 + pkgs.python27Packages.pytestcov + PyTestInstaFail + pkgs.python27Packages.simplejson + pkgs.python27Packages.requests2 + pkgs.python27Packages.requests_toolbelt + pkgs.python27Packages.requests_oauthlib + pkgs.python27Packages.six + pkgs.python27Packages.sphinx + pkgs.python27Packages.tlslite + Ordereddict + ]; + src = fetchurl { + url = "https://pypi.python.org/packages/source/j/jira/${name}.tar.gz"; + md5 = "23abea2446beb4161ce50bab13654319"; + }; + doCheck = false; +}; + + +PyElasticsearch = buildPythonPackage rec { + name = "pyelasticsearch-1.4"; + propagatedBuildInputs = [ + pkgs.python27Packages.six + pkgs.python27Packages.simplejson + pkgs.python27Packages.urllib3 + pkgs.python27Packages.elasticsearch + pkgs.python27Packages.certifi + pkgs.python27Packages.nose + pkgs.python27Packages.mock + ]; + src = fetchurl { + url = "https://pypi.python.org/packages/source/p/pyelasticsearch/${name}.tar.gz"; + md5 = "ed61ebb7b253364e55b4923d11e17049"; + }; +}; + + +PyramidAuthStack = buildPythonPackage rec { + name = "pyramid_authstack-1.0.1"; + propagatedBuildInputs = [ + pkgs.python27Packages.pyramid + pkgs.python27Packages.mock + ]; + src = fetchurl { + url = "https://pypi.python.org/packages/source/p/pyramid_authstack/${name}.tar.gz"; + md5 = "8e199862b5a5cd6385f7d5209cee2f12"; + }; +}; + + +PyramidRedisSessions = buildPythonPackage rec { + name = "pyramid_redis_sessions-1.0.1"; + propagatedBuildInputs = [ + pkgs.python27Packages.pyramid + pkgs.python27Packages.pytest + pkgs.python27Packages.mock + Redis + ]; + src = fetchurl { + url = "https://pypi.python.org/packages/source/p/pyramid_redis_sessions/${name}.tar.gz"; + md5 = "a39bbfd36f61685eac32d5f4010d3fef"; + }; +}; + + +RedlockPy = buildPythonPackage rec { + name = "redlock-py-1.0.5"; + propagatedBuildInputs = [ + Redis + ]; + src = fetchurl { + url = "https://pypi.python.org/packages/source/r/redlock-py/${name}.tar.gz"; + md5 = "be9da24bf9b360d56ff2b66476879c86"; + }; +}; + +Velruse = buildPythonPackage rec { + name = "velruse-1.1.1"; + propagatedBuildInputs = [ + AnyKeystore + PythonOpenId + pkgs.python27Packages.pyramid + pkgs.python27Packages.unittest2 + pkgs.python27Packages.requests2 + pkgs.python27Packages.requests_oauthlib + pkgs.python27Packages.webtest + pkgs.python27Packages.selenium + ]; + src = fetchurl { + url = "https://pypi.python.org/packages/source/v/velruse/${name}.tar.gz"; + md5 = "40cc41048817e248d9292933be194eeb"; + }; + doCheck = false; # setting up selenium for this might be complicated :P +}; + + +WTForms = buildPythonPackage rec { + name = "WTForms-2.0.2"; + src = fetchurl { + url = "https://pypi.python.org/packages/source/W/WTForms/${name}.zip"; + md5 = "613cf723ab40537705bec02733c78d95"; + }; +}; + + +Paginate = buildPythonPackage rec { + name = "paginate-0.5"; + src = fetchurl { + url = "https://pypi.python.org/packages/source/p/paginate/${name}.tar.gz"; + md5 = "1f95c4440819d776e3b9ce7dc8dc40a4"; + }; +}; + + +PaginateSQLAlchemy = buildPythonPackage rec { + name = "paginate_sqlalchemy-0.2.0"; + propagatedBuildInputs = [ + Paginate + pkgs.python27Packages.sqlalchemy9 + ]; + buildInputs = [ + pkgs.python27Packages.sqlalchemy9 + ]; + src = fetchurl { + url = "https://pypi.python.org/packages/any/p/paginate_sqlalchemy/${name}.tar.gz"; + md5 = "4ca097c4132f43cd72c6a1795b6bbb5d"; + }; +}; + + +ZigguratFoundations = buildPythonPackage rec { + name = "ziggurat_foundations-0.6.2"; + propagatedBuildInputs = [pkgs.python27Full]; + buildInputs = [ + pkgs.python27Full + pkgs.python27Packages.six + pkgs.python27Packages.alembic + pkgs.python27Packages.cryptacular + pkgs.python27Packages.coverage + PaginateSQLAlchemy + ]; + src = fetchurl { + url = "http://pypi.python.org/packages/source/z/ziggurat-foundations/${name}.tar.gz"; + md5 = "424cf8740f87b12e4728269912f9fc0b"; + }; + doCheck = false; +}; + +AppEnlight = buildPythonPackage rec { + name = "appenlight"; + propagatedBuildInputs = [ + pkgs.python27Full + pkgs.python27Packages.alembic + AppEnlightClient + Camplight + pkgs.python27Packages.celery + pkgs.python27Packages.colander + pkgs.python27Packages.cryptacular + DefusedXML + pkgs.python27Packages.dogpile_cache + FormEncode + pkgs.python27Packages.gevent + pkgs.python27Packages.gevent-websocket + pkgs.python27Packages.gunicorn + JIRA + pkgs.python27Packages.lxml + pkgs.python27Packages.markdown + pkgs.python27Packages.mock + PaginateSQLAlchemy + pkgs.python27Packages.psutil + pkgs.python27Packages.psycopg2 + PyElasticsearch + pkgs.python27Packages.pygments + pkgs.python27Packages.pyramid + pkgs.python27Packages.pyramid_debugtoolbar + PyramidAuthStack + pkgs.python27Packages.itsdangerous + pkgs.python27Packages.jinja2 + pkgs.python27Packages.pyramid_jinja2 + pkgs.python27Packages.pyramid_mailer + PyramidRedisSessions + pkgs.python27Packages.pyramid_tm + pkgs.python27Packages.dateutil + pkgs.python27Packages.redis + RedlockPy + pkgs.python27Packages.requests2 + pkgs.python27Packages.requests_oauthlib + pkgs.python27Packages.simplejson + pkgs.python27Packages.six + pkgs.python27Packages.sqlalchemy9 + pkgs.python27Packages.transaction + Velruse + WTForms + pkgs.python27Packages.waitress + pkgs.python27Packages.webhelpers + ZigguratFoundations + pkgs.python27Packages.zope_sqlalchemy + ]; + src = ./.; + doCheck= false; +}; + + +in buildPythonPackage { + name = "appenlight-env"; + buildInputs = [ + AppEnlight + ]; + src = ./.; + doCheck=false; +} diff --git a/development.ini b/development.ini new file mode 100644 index 0000000..f6f50f5 --- /dev/null +++ b/development.ini @@ -0,0 +1,196 @@ +[app:appenlight] +use = egg:appenlight +reload_templates = true +debug_authorization = true +debug_notfound = true +debug_routematch = true +debug_templates = true +default_locale_name = en +sqlalchemy.url = postgresql://test:test@localhost/appenlight +sqlalchemy.pool_size = 10 +sqlalchemy.max_overflow = 50 +sqlalchemy.echo = false +jinja2.directories = appenlight:templates +jinja2.filters = nl2br = appenlight.lib.jinja2_filters.nl2br + + +pyramid.includes = pyramid_debugtoolbar + +debugtoolbar.panels = + pyramid_debugtoolbar.panels.versions.VersionDebugPanel + pyramid_debugtoolbar.panels.settings.SettingsDebugPanel + pyramid_debugtoolbar.panels.headers.HeaderDebugPanel + pyramid_debugtoolbar.panels.renderings.RenderingsDebugPanel + pyramid_debugtoolbar.panels.logger.LoggingPanel + pyramid_debugtoolbar.panels.performance.PerformanceDebugPanel + pyramid_debugtoolbar.panels.routes.RoutesDebugPanel + pyramid_debugtoolbar.panels.sqla.SQLADebugPanel + pyramid_debugtoolbar.panels.tweens.TweensDebugPanel + pyramid_debugtoolbar.panels.introspection.IntrospectionDebugPanel + +appenlight.includes = + +# encryption +encryption_secret = oEOikr_T98wTh_xLH3w8Se3kmbgAQYSM4poZvPosya0= + +#redis +redis.url = redis://localhost:6379/0 +redis.redlock.url = redis://localhost:6379/3 + +#solr +elasticsearch.nodes = http://127.0.0.1:9200 + +#dirs +webassets.dir = %(here)s/webassets/ + +#authtkt +authtkt.secure = false +authtkt.secret = SECRET +# session settings +redis.sessions.secret = SECRET +redis.sessions.timeout = 86400 + +# session cookie settings +redis.sessions.cookie_name = appenlight +redis.sessions.cookie_max_age = 2592000 +redis.sessions.cookie_path = / +redis.sessions.cookie_domain = +redis.sessions.cookie_secure = False +redis.sessions.cookie_httponly = False +redis.sessions.cookie_on_exception = True +redis.sessions.prefix = appenlight:session: + + +#cache +cache.regions = default_term, second, short_term, long_term +cache.type = ext:memcached +cache.url = 127.0.0.1:11211 +cache.lock_dir = %(here)s/data/cache/lock +cache.second.expire = 1 +cache.short_term.expire = 60 +cache.default_term.expire = 300 + +#mailing +mailing.app_url = http://localhost:6543 +mailing.from_name = App Enlight LOCAL +mailing.from_email = no-reply@appenlight.local + + +### +# Authomatic configuration +### + +authomatic.secret = SECRET +authomatic.pr.facebook.app_id = +authomatic.pr.facebook.secret = +authomatic.pr.twitter.key = +authomatic.pr.twitter.secret = +authomatic.pr.google.key = +authomatic.pr.google.secret = +authomatic.pr.github.key = +authomatic.pr.github.secret = +authomatic.pr.github.scope = repo, public_repo, user:email +authomatic.pr.bitbucket.key = +authomatic.pr.bitbucket.secret = + +#ziggurat +ziggurat_foundations.model_locations.User = appenlight.models.user:User +ziggurat_foundations.sign_in.username_key = sign_in_user_name +ziggurat_foundations.sign_in.password_key = sign_in_user_password +ziggurat_foundations.sign_in.came_from_key = came_from + +#cometd +cometd.server = http://127.0.0.1:8088/ +cometd.secret = secret +cometd.ws_url = http://127.0.0.1:8088/ + + +# for celery +appenlight.api_key = +appenlight.transport_config = +appenlight.public_api_key = + +celery.broker_type = redis +celery.broker_url = redis://localhost:6379/3 +celery.concurrency = 4 +celery.timezone = UTC + + +[filter:paste_prefix] +use = egg:PasteDeploy#prefix + + +[filter:appenlight_client] +use = egg:appenlight_client +appenlight.api_key = +appenlight.transport_config = +appenlight.report_local_vars = true +appenlight.report_404 = true +appenlight.logging.level = DEBUG +appenlight.timing.dbapi2_psycopg2 = 0.3 + + +[pipeline:main] +pipeline = + paste_prefix + appenlight_client + appenlight + + + +[server:main] +use = egg:waitress +host = 0.0.0.0 +port = 6543 + +[server:main_prod] +use = egg:gunicorn#main +host = 0.0.0.0:6543, unix:/tmp/appenlight.sock +workers = 6 +timeout = 90 +#max_requests = 1000 + + +# Begin logging configuration + +[loggers] +keys = root, appenlight, sqlalchemy, elasticsearch + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_appenlight] +level = INFO +handlers = +qualname = appenlight + +[logger_elasticsearch] +level = WARN +handlers = +qualname = elasticsearch + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/frontend/.jshintrc b/frontend/.jshintrc new file mode 100644 index 0000000..dd491c8 --- /dev/null +++ b/frontend/.jshintrc @@ -0,0 +1,56 @@ +{ + /* + * ENVIRONMENTS + * ================= + */ + + // Define globals exposed by modern browsers. + "browser": true, + + // Define globals exposed by Node.js. + "node": false, + + // Allow ES6. + "esnext": true, + + /* + * ENFORCING OPTIONS + * ================= + */ + + // Force all variable names to use either camelCase style or UPPER_CASE + // with underscores. + "camelcase": true, + + // Prohibit use of == and != in favor of === and !==. + "eqeqeq": true, + + // Enforce tab width of 2 spaces. + "indent": 2, + + // Prohibit use of a variable before it is defined. + "latedef": true, + + // Enforce line length to 100 characters + "maxlen": 100, + + // Require capitalized names for constructor functions. + "newcap": true, + + // Enforce use of single quotation marks for strings. + "quotmark": "single", + + // Enforce placing 'use strict' at the top function scope + "strict": true, + + // Prohibit use of explicitly undeclared variables. + "undef": true, + + // Warn when variables are defined but never used. + "unused": true, + + "globals": { + "TraceKit": true, + "define": true + } +} diff --git a/frontend/Gruntfile.js b/frontend/Gruntfile.js new file mode 100644 index 0000000..09f29bd --- /dev/null +++ b/frontend/Gruntfile.js @@ -0,0 +1,151 @@ +var fs = require('fs'); +var ini = require('ini'); +var config = ini.parse(fs.readFileSync('./locations.ini', 'utf-8')) +module.exports = function (grunt) { + + var grunt_conf_obj = { + pkg: grunt.file.readJSON('package.json'), + + ngtemplates: { + app: { + options: { + module: 'appenlight.templates' + }, + cwd: "src", + src: '**/*.html', + dest: 'build/templates.js' + } + }, + + concat: { + options: { + // define a string to put between each file in the concatenated output + separator: '\n;' + }, + base: { + src: [ + "bower_components/underscore/underscore.js", + "bower_components/angular/angular.min.js", + "bower_components/angular-cookies/angular-cookies.min.js", + "bower_components/angular-route/angular-route.min.js", + "bower_components/angular-resource/angular-resource.min.js", + "bower_components/angular-animate/angular-animate.min.js", + "bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js", + "bower_components/angular-ui-router/release/angular-ui-router.min.js", + "bower_components/angular-toArrayFilter/toArrayFilter.js", + "vendors/crel.js", + "bower_components/json-human/src/json.human.js", + "bower_components/moment/min/moment.min.js", + "bower_components/d3/d3.min.js", + "bower_components/c3/c3.min.js", + "bower_components/angular-smart-table/dist/smart-table.min.js", + "bower_components/ment.io/dist/mentio.min.js", + "vendors/simple_moment_utc.js", + "vendors/reconnecting-websocket.js", + ], + dest: "build/base.js", + nonull: true + } + , + dev: { + src: [ + "src/utils.js", + "src/app.js", + "build/templates.js", + "src/**/*.js", + "!src/**/*_test.js" + ], + dest: 'build/app.js', + nonull: true + }, + dist: { + src: [ + 'build/base.js', + 'build/app.js' + ], + dest: "build/release/js/appenlight.js", + nonull: true + }, + }, + removelogging: { + dist: { + src: "build/app.js" + } + }, + copy: { + css: { + files: [ + // includes files within path and its sub-directories + { + expand: true, + cwd: 'build/release/css', + src: ['front.css'], + dest: config.ae_statics_location + '/css' + }, + { + expand: true, + cwd: 'build/release/css', + src: ['front.css'], + dest: config.ae_webassets_location + '/appenlight/css' + } + ] + }, + js: { + files: [ + // includes files within path and its sub-directories + { + expand: true, + cwd: 'build/release/js', + src: ['**'], + dest: config.ae_statics_location + '/js' + }, + { + expand: true, + cwd: 'build/release/js', + src: ['**'], + dest: config.ae_webassets_location + '/appenlight/js' + } + ] + } + }, + watch: { + dev: { + files: ['<%= concat.dev.src %>', 'src/**/*.html', '!build/*.js'], + tasks: ['ngtemplates', 'concat:dev', 'concat:dist', 'copy:js'] + }, + css: { + files: ['css/**/*.less', 'css/**/*.css'], + tasks: ['less', 'copy:css'] + } + }, + + less: { + dev: { + files: { + "build/release/css/front.css": "css/front_app.less", + "build/release/css/front_landing.css": "css/front_landing.less" + } + } + } + + }; + + grunt.initConfig(grunt_conf_obj); + + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-bower-concat'); + grunt.loadNpmTasks('grunt-contrib-requirejs'); + grunt.loadNpmTasks('grunt-contrib-copy'); + grunt.loadNpmTasks("grunt-remove-logging"); + grunt.loadNpmTasks('grunt-angular-templates'); + grunt.loadNpmTasks('grunt-contrib-less'); + + + grunt.registerTask('styles', ['less']); + grunt.registerTask('test', ['jshint', 'qunit']); + + grunt.registerTask('default', ['ngtemplates', 'concat:base', 'concat:dev', 'removelogging', 'concat:dist', 'less', 'copy:js', 'copy:css']); + +}; diff --git a/frontend/bower.json b/frontend/bower.json new file mode 100644 index 0000000..32d215c --- /dev/null +++ b/frontend/bower.json @@ -0,0 +1,44 @@ +{ + "name": "appenlight", + "version": "0.1", + "authors": [ + "Marcin Lulek " + ], + "description": "Appenlight main JS files", + "main": "appenlight.js", + "moduleType": [ + "amd" + ], + "license": "Properietary", + "homepage": "https://appenlight.com", + "private": true, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "angular": "1.5.5", + "angular-resource": "1.5.5", + "angular-cookies": "1.5.5", + "angular-sanitize": "1.5.5", + "angular-animate": "1.5.5", + "angular-touch": "1.5.5", + "angular-route": "1.5.5", + "angular-messages": "1.5.5", + "angular-mocks": "1.5.5", + "angular-scenario": "1.5.5", + "angular-bootstrap": "1.3.2", + "angular-ui-router": "1.0.0-alpha.5", + "angular-toArrayFilter" : "1.0.1", + "d3": "3.5.0", + "c3": "0.4.11", + "underscore": "~1.6.0", + "json-human": "*", + "moment": "~2.8.1", + "angular-smart-table": "v2.1.8", + "ment.io": "0.9.24" + } +} diff --git a/frontend/css/codehilite.css.less b/frontend/css/codehilite.css.less new file mode 100644 index 0000000..a99acf9 --- /dev/null +++ b/frontend/css/codehilite.css.less @@ -0,0 +1,62 @@ +.codehilite .hll { background-color: #ffffcc } +.codehilite { background: #ffffff; } +.codehilite .c { color: #888888 } /* Comment */ +.codehilite .err { color: #FF0000; } /* Error */ +.codehilite .k { color: #008800; font-weight: bold } /* Keyword */ +.codehilite .o { color: #333333 } /* Operator */ +.codehilite .cm { color: #888888 } /* Comment.Multiline */ +.codehilite .cp { color: #557799 } /* Comment.Preproc */ +.codehilite .c1 { color: #888888 } /* Comment.Single */ +.codehilite .cs { color: #cc0000; font-weight: bold } /* Comment.Special */ +.codehilite .gd { color: #A00000 } /* Generic.Deleted */ +.codehilite .ge { font-style: italic } /* Generic.Emph */ +.codehilite .gr { color: #FF0000 } /* Generic.Error */ +.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.codehilite .gi { color: #00A000 } /* Generic.Inserted */ +.codehilite .go { color: #888888 } /* Generic.Output */ +.codehilite .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ +.codehilite .gs { font-weight: bold } /* Generic.Strong */ +.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.codehilite .gt { color: #0044DD } /* Generic.Traceback */ +.codehilite .kc { color: #008800; font-weight: bold } /* Keyword.Constant */ +.codehilite .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */ +.codehilite .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */ +.codehilite .kp { color: #003388; font-weight: bold } /* Keyword.Pseudo */ +.codehilite .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */ +.codehilite .kt { color: #333399; font-weight: bold } /* Keyword.Type */ +.codehilite .m { color: #6600EE; font-weight: bold } /* Literal.Number */ +.codehilite .s { color: #7D560A;} /* Literal.String */ +.codehilite .na { color: #0000CC } /* Name.Attribute */ +.codehilite .nb { color: #007020 } /* Name.Builtin */ +.codehilite .nc { color: #BB0066; font-weight: bold } /* Name.Class */ +.codehilite .no { color: #003366; font-weight: bold } /* Name.Constant */ +.codehilite .nd { color: #555555; font-weight: bold } /* Name.Decorator */ +.codehilite .ni { color: #880000; font-weight: bold } /* Name.Entity */ +.codehilite .ne { color: #FF0000; font-weight: bold } /* Name.Exception */ +.codehilite .nf { color: #0066BB; font-weight: bold } /* Name.Function */ +.codehilite .nl { color: #997700; font-weight: bold } /* Name.Label */ +.codehilite .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ +.codehilite .nt { color: #007700 } /* Name.Tag */ +.codehilite .nv { color: #996633 } /* Name.Variable */ +.codehilite .ow { color: #000000; font-weight: bold } /* Operator.Word */ +.codehilite .w { color: #bbbbbb } /* Text.Whitespace */ +.codehilite .mf { color: #6600EE; font-weight: bold } /* Literal.Number.Float */ +.codehilite .mh { color: #005588; font-weight: bold } /* Literal.Number.Hex */ +.codehilite .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ +.codehilite .mo { color: #4400EE; font-weight: bold } /* Literal.Number.Oct */ +.codehilite .sb { background-color: #fff0f0 } /* Literal.String.Backtick */ +.codehilite .sc { color: #0044DD } /* Literal.String.Char */ +.codehilite .sd { color: #DD4422 } /* Literal.String.Doc */ +.codehilite .s2 { color: #7D560A } /* Literal.String.Double */ +.codehilite .se { color: #666666; font-weight: bold; background-color: #fff0f0 } /* Literal.String.Escape */ +.codehilite .sh { background-color: #fff0f0 } /* Literal.String.Heredoc */ +.codehilite .si { background-color: #eeeeee } /* Literal.String.Interpol */ +.codehilite .sx { color: #DD2200; background-color: #fff0f0 } /* Literal.String.Other */ +.codehilite .sr { color: #000000; background-color: #fff0ff } /* Literal.String.Regex */ +.codehilite .s1 { color: #7D560A } /* Literal.String.Single */ +.codehilite .ss { color: #AA6600 } /* Literal.String.Symbol */ +.codehilite .bp { color: #007020 } /* Name.Builtin.Pseudo */ +.codehilite .vc { color: #336699 } /* Name.Variable.Class */ +.codehilite .vg { color: #dd7700; font-weight: bold } /* Name.Variable.Global */ +.codehilite .vi { color: #3333BB } /* Name.Variable.Instance */ +.codehilite .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */ diff --git a/frontend/css/custom_bootstrap_variables.less b/frontend/css/custom_bootstrap_variables.less new file mode 100644 index 0000000..a37e6dc --- /dev/null +++ b/frontend/css/custom_bootstrap_variables.less @@ -0,0 +1,2 @@ +@grid-gutter-width: 10px; +@table-bg-accent: none; diff --git a/frontend/css/front_app.less b/frontend/css/front_app.less new file mode 100644 index 0000000..57384c7 --- /dev/null +++ b/frontend/css/front_app.less @@ -0,0 +1,23 @@ +@import "front_shared.less"; +@import "sections/applications.css.less"; +@import "sections/dashboard.css.less"; +@import "sections/front_dashboard.css.less"; +@import "sections/logs.css.less"; +@import "sections/register.css.less"; +@import "sections/reports.css.less"; +@import "sections/slow_reports.css.less"; +@import "sections/uptime.css.less"; +@import "sections/integrations.css.less"; +@import "sections/events.css.less"; +@import "sections/user.css.less"; +@import "sections/admin/main.css.less"; +@import "sections/admin/users.css.less"; +@import "codehilite.css.less"; +@import "json.human.css.less"; + +@media (min-width: 1200px) { + .container { + width: 100%; + max-width: 1600px; + } +} diff --git a/frontend/css/front_landing.less b/frontend/css/front_landing.less new file mode 100644 index 0000000..308ccec --- /dev/null +++ b/frontend/css/front_landing.less @@ -0,0 +1,5 @@ +@import "front_shared.less"; +@import "sections/features.css.less"; +@import "sections/index.css.less"; +@import "sections/pages.css.less"; +@import "codehilite.css.less"; diff --git a/frontend/css/front_shared.less b/frontend/css/front_shared.less new file mode 100644 index 0000000..15a3260 --- /dev/null +++ b/frontend/css/front_shared.less @@ -0,0 +1,1041 @@ +@import (inline) "vendors/font-awesome.min.css"; +@import "vendors/less/bootstrap.less"; +@import (inline) "vendors/c3.css"; +@import url(https://fonts.googleapis.com/css?family=Ubuntu:400,500,700); + +@color_dark_blue: rgb(0, 42, 74); +@color_med_blue: rgb(23, 96, 125); +@color_beige: rgb(255, 241, 206); +@color_red: rgb(185, 15, 15); +@color_green: rgb(35, 165, 75); +@color_orange: rgb(255, 132, 0); +@color_dark_orange: rgb(214, 71, 0); +@color_vdark_grey: rgb(33, 33, 33); +@color_dark_grey: rgb(51, 51, 51); +@color_grey: rgb(76, 76, 76); +@color_light_grey: rgb(220, 220, 220); +@color_white: rgb(255, 255, 255); +@color_black: rgb(0, 0, 0); +@color_secondary: lighten(@color_grey, 22%); +@color_header: rgb(97, 92, 99); + +@margin_size: 15px; + +.color-secondary { + color: @color_secondary +} + +.color-header { + color: @color_header; +} + +/**** UTILS ******/ + +.box-shadow (@x: 0, @y: 0, @blur: 5px, @color: rgba(77,77,77,0.5)) { + box-shadow: @arguments; + -o-box-shadow: @arguments; + -ms-box-shadow: @arguments; + -moz-box-shadow: @arguments; + -webkit-box-shadow: @arguments; +} + +.inset-box-shadow (@x: 0, @y: 0, @blur: 5px, @color: rgba(77,77,77,0.5)) { + box-shadow: inset @arguments; + -o-box-shadow: inset @arguments; + -ms-box-shadow: inset @arguments; + -moz-box-shadow: inset @arguments; + -webkit-box-shadow: inset @arguments; +} + +.border-radius (@radius: 5px) { + border-radius: @radius; + -o-border-radius: @radius; + -ms-border-radius: @radius; + -moz-border-radius: @radius; + -webkit-border-radius: @radius; +} + +.custom-border-radius (@topleft: 5px, @topright:5px, @bottomright:5px, @bottomleft:5px) { + -o-border-radius: @topleft @topright @bottomright @bottomleft; + -webkit-border-radius: @topleft @topright @bottomright @bottomleft; + border-radius: @topleft @topright @bottomright @bottomleft; +} + +.transition-duration(@duration: 250ms, @property: all) { + -webkit-transition: @property @duration; + -moz-transition: @property @duration; + -o-transition: @property @duration; + transition: @property @duration; +} + +/************************** OUR CODE ****************************************/ + +body { + background-color: rgb(245, 245, 245); + height: 100%; + margin: 0px; + padding: 0px; + font-family: 'Ubuntu', "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +html { + height: 100%; +} + +.container-fluid { + +} + +#holder { + min-height: 100%; + position: relative; + padding-top: 65px; + // max-width: 1600px; + margin: 0px auto; + #outer-content { + padding-bottom: 270px; + } + +} + +@media (max-width: 767px) { + #outer-content { + padding-bottom: 700px !important; + } +} + +.footer { + position: absolute; + bottom: 0; + width: 100%; + /* Set the fixed height of the footer here */ + min-height: 250px; + padding: 35px 15px 15px 15px; + background-image: url("/static/appenlight/images/footer_bg.png"); + color: @color_white; + + strong { + font-family: 'Ubuntu', sans-serif; + } + + ul { + padding: 0px; + li { + list-style: none; + } + } + + a { + color: @color_white !important; + &:link, &:visited { + color: @color_white !important + } + } + + hr { + background-color: #382f2d; + margin: 10px 0px; + border-bottom: 1px solid #4a454d; + } + +} + +.full-width { + width: 100%; +} + +#container-footer-push { + height: 75px; +} + +#container-wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0px auto -65px; +} + +h1 { + &.page-title { + border-bottom: 2px solid rgb(255, 111, 0); + } +} + +a:not(.btn):link, a:not(.btn):visited, a:not(.btn) { + color: @color_med_blue; + text-decoration: none; + cursor: pointer; + + &:hover { + color: @color_dark_blue; + cursor: pointer; + } + +} + +.user-unlogged { + + #holder { + padding-top: 125px; + } + + .navbar-fixed-top { + position: absolute; + min-height: 125px; + background-image: url("/static/appenlight/images/nav_bg.jpg"); + background-repeat: no-repeat; + background-position: center; + border: 0px; + background-color: transparent; + .pattern { + background-image: url(/static/appenlight/images/dots2_bg.png); + max-width: 1600px; + margin: 0px auto; + min-height: 125px; + } + + .btn-orange { + margin-top: 35px + + } + + .navbar-nav { + margin-left: 0px !important; + margin-right: 0px !important; + } + + } + + .nav > li > a { + margin-top: 35px; + color: @color_white; + &:link, &:visited { + color: @color_white + } + + } + +} + +#top-navbar { + + .navbar-brand { + padding: 9px 0px 0px 20px + } + + .navbar-nav > li { + float: left; + } + +} + +/* forms */ + +@form_border_color: rgb(181, 188, 199); +@form_hover_color: rgb(229, 242, 254); +@form_hover_border_color: rgb(117, 157, 192); + +.form-error { + border-radius: 3px 3px 3px 3px; + margin-bottom: 5px !important; + padding: 5px 10px; +} + +.SelectField, .SubmitField, .BooleanField, input[type=checkbox], input[type=submit], input[type=select], button { + width: auto; +} + +/* flash */ +.flashMessages { + padding: 0px; + margin: 0px; +} + +.alert-notice { + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-warning { + color: #c09853; + background-color: #fcf8e3; + border-color: #fbeed5; +} + +.alert-error { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} + +/* icons */ +.icon { + background-repeat: no-repeat; + width: 16px; + height: 16px; + display: -moz-inline-box; + display: inline-block; + vertical-align: middle; + margin: 0px 5px 0px 5px; + background-position: center center; +} + +.icon.big { + background-repeat: no-repeat; + width: 32px; + height: 32px; + display: -moz-inline-box; + display: inline-block; + vertical-align: middle; + margin: 0px 5px 0px 5px; +} + +/******************* tables ****************/ + +.table-striped { + > tbody > tr:nth-of-type(even) { + background-color: lighten(@color_light_grey, 12); + } +} + +.table { + table-layout: fixed; + + + + + caption { + color: @color_grey; + font-weight: bold; + text-align: center; + background-color: lighten(@color_light_grey, 10%); + } + + thead { + background-color: lighten(@color_light_grey, 10%); + } + + caption a:link, table.stylized caption a:visited { + color: #ffffff; + text-decoration: none; + font-weight: bold; + } + + caption a:link, table.stylized caption a:hover { + color: #ffcc00; + text-decoration: none; + font-weight: bold; + } + + thead > tr > th { + border-bottom: 1px solid darken(@color_light_grey, 2%) !important; + font-size: 86%; + } + + tbody > tr > td { + border-top: 0px; + vertical-align: middle; + } + + .no { + width: 30px; + } + + td.ordering.dsc, td.ordering.asc { + padding-right: 20px; + .transition-duration; + /* position: relative; */ + } + + td.ordering { + .marker { + display: block; + float: right; + height: 10px; + margin: -13px -15px 0px 0; + width: 10px; + background-repeat: no-repeat; + + } + &.asc .marker { + background-image: url("/static/appenlight/images/dark_asc.png"); + + } + &.dsc .marker { + background-image: url("/static/appenlight/images/dark_dsc.png"); + + } + a:link, a:visited { + color: @color_vdark_grey !important; + font-weight: bold; + text-decoration: underline; + } + } + +} + +.btn.orange-special { + .border-radius(5px); + background-color: @color_orange; + font-size: 100%; + padding: 10px !important; + .box-shadow(0, 0, 5px, rgba(77, 77, 77, 0.25)); + color: rgb(255, 255, 255) !important; + text-shadow: 0 1px 1px #5F1C00; + border: 1px solid rgb(236, 86, 15); + background: rgb(255, 123, 13); /* Old browsers */ + text-transform: uppercase; + display: inline-block; + .transition-duration; +} + +.btn.orange-special:hover { + .box-shadow(0, 0, 15px, rgba(255, 114, 42, 0.75)); + border: 1px solid rgb(209, 114, 42); + +} + +.sign-in-form { + position: relative; + margin: 0px; + fieldset { + border: 0px !important; + display: inline-block; + } + .action_links { + position: absolute; + top: 4px; + right: 10px; + li { + list-style: none; + } + } + + legend { + display: none; + } + .form-fields { + padding: 0px; + margin: 0px; + li { + display: inline-block; + &#row-sign_in_user_name, &#sign_in_user_password { + width: 100px; + margin: 0px 15px 0px 0px; + } + + #sign_in_user_name { + width: 90px; + } + + #sign_in_user_password { + width: 90px; + } + } + } +} + +.word-wrap { + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ + + &.break-all { + word-break: break-all; + } +} + +pre, code { + background-color: rgb(255, 255, 255); + color: @color_grey; + font-family: monospace; +} + +/* common in section */ + +.errors-small-list { + + width: 100%; + table-layout: fixed; + + td { + padding: 0px 5px 10px 5px; + } + + td.occurences { + width: 40px; + } + + .duration { + font-size: 80%; + background-color: darken(@color_white, 10%); + padding: 2px 5px; + display: inline-block; + color: #000000; + .border-radius(2px); + margin: 0px 5px 0px 2px; + } + + span.occurences { + font-size: 86%; + font-weight: bold; + background-color: @color_orange; + border-radius: 3px; + color: #ffffff; + display: inline-block; + min-height: 33px; + min-width: 33px; + padding: 8px 0 0; + text-align: center; + } + + a.error-type { + font-weight: bold; + font-size: 86%; + } + .url { + color: #777777; + font-size: 80%; + } + td.info { + width: 150px; + font-size: 70%; + } + td .report { + line-height: 100%; + } +} + +/****** paginator ********/ + +.paginator { + margin: 5px 0px 5px 0px; + padding: 5px 0px 5px 0px; + font-size: 80%; + text-align: right; + a, span { + margin: 0px 6px 0px 2px; + padding: 3px 7px; + text-decoration: none; + .border-radius(15px); + border: 1px solid @color_light_grey; + .box-shadow(0px, 0px, 5px, transparent); + background-color: #ffffff; + .transition-duration(); + &.pager_curpage { + font-weight: bold; + background-color: #FF6F00; + color: #ffffff; + border: 3px solid #ff4e00; + } + &.pager_link:link, &.pager_link:visited, .prev, .next { + background-color: #ffffff; + color: #000000; + } + + &.pager_link:hover, &:hover { + background-color: #FF6F00; + color: #ffffff; + border: 1px solid #FF6F00; + } + &.pager_dotdot { + background-color: transparent; + border: 0px; + .box-shadow(0px, 0px, 5px, transparent); + color: #000000; + } + } +} + +.notFoundPage #content { + .heading-text { + font-size: 250%; + } +} + +.errorPage #content { + .heading-text { + font-size: 250%; + } +} + +.forbiddenPage #content { + .heading-text { + font-size: 250%; + } +} + +.ajax_loader_3 { + width: 66px; + height: 66px; + background-image: url('../images/ajax_loader_3.gif'); +} + +.clear { + clear: both; +} + +/***** content positioning *****/ + +.position-absolute { + position: absolute; +} + +.position-relative { + position: relative; +} + +.increse-zindex { + z-index: 500; +} + +.m-x-auto { + margin-right: auto !important; + margin-left: auto !important; +} + +.gen-margins( @counter ) when ( @counter < 11 ) { + .gen-margins(@counter + 1); // iterate. + // do the thing here. + + .m-a-@{counter} { + margin: @margin_size * @counter !important; + } + + .m-t-@{counter} { + margin-top: @margin_size * @counter !important; + } + .m-l-@{counter} { + margin-left: @margin_size * @counter !important; + } + .m-r-@{counter} { + margin-right: @margin_size * @counter !important; + } + + .m-b-@{counter} { + margin-bottom: @margin_size * @counter !important; + } + + .p-a-@{counter} { + padding: @margin_size * @counter !important; + } + + .p-t-@{counter} { + padding-top: @margin_size * @counter !important; + } + .p-l-@{counter} { + padding-left: @margin_size * @counter !important; + } + .p-r-@{counter} { + padding-right: @margin_size * @counter !important; + } + .p-b-@{counter} { + padding-bottom: @margin_size * @counter !important; + } + +} + +.gen-margins(0); + +hr { + background-color: @color_light_grey; + border: 0px; + height: 1px; +} + +.white-block() { + padding: 20px; + background-color: @color_white; + border: 1px solid #dddddd; + border-radius: 4px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.green-block() { + padding: 20px; + background-color: @color_orange; + border: 1px solid @color_light_grey +} + +.blue-block() { + margin: 20px 0px; + padding: 20px; + background-color: @color_med_blue; + border: 1px solid @color_light_grey +} + +h1, h2, h3, h4 { + font-weight: normal; +} + +h1 { + font-size: 2em; +} + +h2 { + font-size: 1.8em; +} + +h3 { + font-size: 1.6em; +} + +h4 { + font-size: 1.4em; +} + +.alert:last-of-type { + margin-bottom: 0px; +} + +#content { +} + +#menu { + .header { + margin: 0px 0px 10px 0px; + font-size: 86%; + text-transform: uppercase; + font-weight: bold; + } + label { + font-size: 86%; + } + + form { + margin-bottom: 0px; + } + + .form-fields { + padding: 0px; + } + + .TextField, .SelectField, .PasswordField { + width: 170px; + } + + .panel-heading { + font-weight: bold; + } +} + +.ellipsis { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + -o-text-overflow: ellipsis; + -ms-text-overflow: ellipsis; +} + +.hidden { + display: none; +} + +.dim { + opacity: 0.5 +} + +/* GRAPHS */ + +.graphs, .graph { + position: relative; +} + +.bg-3 { + background-image: url('/static/appenlight/images/px_by_Gre3g.png'); +} + +.bg-2.pad-bottom, .bg-3.pad-bottom { + padding-bottom: 20px; +} + +.codehilite pre { + border-radius: 5px; + margin: 20px 0px; +} + +.admonition { + margin: 0px 0px 10px 0px; + .white-block; + + .admonition-title { + font-weight: bold; + } + &.important .admonition-title { + color: @color_red; + } + + &.note .admonition-title { + color: @color_green; + } +} + +.white-text { + color: #ffffff; +} + +.perf_stats { + + .stat { + margin-right: 10px; + } + + .bar { + height: 10px; + display: inline-block; + } + .custom { + background-color: rgb(152, 223, 138); + } + .tmpl { + background-color: rgb(75, 207, 75); + } + .remote { + background-color: rgb(255, 187, 120); + } + .nosql { + background-color: rgb(255, 127, 14); + } + .sql { + background-color: rgb(174, 199, 232); + } + .main { + background-color: rgb(40, 152, 230); + } +} + +.pagination .active > a, +.pagination .active > span, +.pagination .active > a:hover, +.pagination .active > span:hover, +.pagination .active > a:focus, +.pagination .active > span:focus { + z-index: 2; + color: #ffffff; + cursor: pointer; + background-color: @color_orange; + background-color: @color_dark_orange; + border-color: @color_dark_orange; +} + +.pagination .disabled > span, +.pagination .disabled > a, +.pagination .disabled > a:hover, +.pagination .disabled > a:focus { + color: #999999; + cursor: not-allowed; + background-color: #ffffff; + border-color: #dddddd; +} + +.search-params .tag { + border-radius: 5px 5px 5px 5px; + display: inline-block; + margin: 0px 5px 5px 0; + padding: 5px 5px; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); + background-color: #D9EDF7; + border: 1px solid #BCE8F1; + color: #3A87AD; + a { + color: #3A87AD; + } +} + +.user-assignment { + border: 1px solid #eeeeee; + padding: 5px; + border-radius: 3px; + vertical-align: top; + cursor: pointer; + img { + vertical-align: top; + max-height: 50px; + float: left; + margin-right: 7px; + border-radius: 60px; + } + &:hover { + border: 1px solid #aaeeff; + } +} + +.graphs { + min-height: 50px; +} + +.panel-heading { + background: -moz-linear-gradient(top, rgba(252, 254, 255, 0) 0%, rgba(0, 0, 0, 0.05) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(252, 254, 255, 0)), color-stop(100%, rgba(0, 0, 0, 0.05))); + background: -webkit-linear-gradient(top, rgba(252, 254, 255, 0) 0%, rgba(0, 0, 0, 0.05) 100%); + background: -o-linear-gradient(top, rgba(252, 254, 255, 0) 0%, rgba(0, 0, 0, 0.05) 100%); + background: -ms-linear-gradient(top, rgba(252, 254, 255, 0) 0%, rgba(0, 0, 0, 0.05) 100%); + background: linear-gradient(to bottom, rgba(252, 254, 255, 0) 0%, rgba(0, 0, 0, 0.05) 100%); +} + +.typeahead-tags { + + a { + border: 1px solid #eeeeee; + padding: 10px 20px !important; + min-width: 400px; + } + + .tag { + font-weight: bold; + strong { + background-color: @color_orange; + color: @color_white; + } + } + + .description { + font-size: 85%; + } + + .example { + font-style: italic; + font-family: monospace; + font-size: 85%; + color: lighten(@color_black, 50%); + } +} + +.orange-dots { + color: @color_orange; + font-size: 5px; + .fa { + &:first-child { + margin: 0px 3px 0px 0px; + } + margin: 0px 3px; + } +} + +.gray-dots { + color: @color_light_grey; + font-size: 5px; + .fa { + &:first-child { + margin: 0px 3px 0px 0px; + } + margin: 0px 3px; + } +} + +.user-unlogged { + #logo-normal { + display: inline-block; + width: 264px; + height: 48px; + background-image: url('/static/appenlight/images/ix-appenlight-logo.png'); + margin-top: 25px; + } + + #logo-icon { + display: inline-block; + width: 53px; + height: 48px; + background-image: url('/static/appenlight/images/ix-appenlight-icon.png'); + margin-top: 25px; + } +} + +.user-logged { + #logo-normal { + display: inline-block; + width: 176px; + height: 32px; + background-image: url('/static/appenlight/images/appenlight-logo.png'); + } + + #logo-icon { + display: inline-block; + width: 32px; + height: 32px; + background-image: url('/static/appenlight/images/appenlight-icon.png'); + } +} + +.btn-green { + background-color: #93b715 !important; + border-color: #93b715 !important; + box-shadow: none !important; + padding: 10px; + text-transform: uppercase; + color: @color_white; + font-weight: bold; +} + +.btn-orange { + background-color: @color_orange !important; + border-color: @color_orange !important; + box-shadow: none !important; + padding: 10px; + text-transform: uppercase; + color: @color_white !important; + font-weight: bold; +} + +.list-group-item.active, .list-group-item.active:hover, .list-group-item.active:focus { + background-color: transparent; + font-weight: bold; + border-color: transparent; +} + +.list-group.sub-group { + margin: -10px -15px; +} + +.sub-group { + .list-group-item { + background-color: #f5f5f5; + } +} + +.panel { + .breadcrumb { + margin-bottom: 0px; + padding: 0px; + background-color: transparent; + + a { + color: @color_dark_grey; + } + + } +} + +.bold { + font-weight: bold; +} + +.table > thead > tr > th { + border-bottom: 1px solid lighten(@color_grey, 33%); +} + +.ng-hide { + display: none; +} + +/* smart table */ +.st-sort-ascent:after { + content: '\25B2'; +} + +.st-sort-descent:after { + content: '\25BC'; +} + +.slim-input { + width: auto; + max-width: 125px; +} + +.input-autosize { + width: auto; + display: inline-block; +} diff --git a/frontend/css/json.human.css.less b/frontend/css/json.human.css.less new file mode 100644 index 0000000..d334cfb --- /dev/null +++ b/frontend/css/json.human.css.less @@ -0,0 +1,78 @@ +.jh-root, .jh-type-object, .jh-type-array, .jh-key, .jh-value, .jh-root tr{ + -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */ + -moz-box-sizing: border-box; /* Firefox, other Gecko */ + box-sizing: border-box; /* Opera/IE 8+ */ +} + +.jh-key, .jh-value{ + margin: 0; + padding: 10px; +} + +.jh-value{ + +} + +.jh-type-bool, .jh-type-number{ + font-weight: bold; + text-align: center; + color: #5286BC; +} + +.jh-type-string{ + font-style: italic; + color: #839B00; + word-break: break-all; +} + +.jh-array-key{ + font-style: italic; + font-size: small; + text-align: center; +} + +.jh-object-key, .jh-array-key{ + color: #444; + vertical-align: top; +} + +.jh-type-object > tr:nth-child(odd), .jh-type-array > tr:nth-child(odd){ + background-color: #f5f5f5; +} + +.jh-type-object > tr:nth-child(even), .jh-type-array > tr:nth-child(even){ + background-color: #fff; +} + +.jh-type-object, .jh-type-array{ + width: 100%; + border-collapse: collapse; +} + +.jh-root{ + margin: 0.2em; +} + +th.jh-key{ + text-align: left; + padding: 10px; + width: 50px; +} + +.jh-type-object > tr, .jh-type-array > tr{ + border-bottom: none; +} + +.jh-type-object > tr:last-child, .jh-type-array > tr:last-child{ + +} + +.jh-type-object > tr:hover, .jh-type-array > tr:hover{ + +} + +.jh-empty{ + font-style: italic; + color: #999; + font-size: small; +} diff --git a/frontend/css/sections/admin/main.css.less b/frontend/css/sections/admin/main.css.less new file mode 100644 index 0000000..1e8673c --- /dev/null +++ b/frontend/css/sections/admin/main.css.less @@ -0,0 +1,8 @@ +.top-state-admin { + @media (min-width: 1200px) { + .container { + width: 100%; + max-width: 1600px; + } + } +} diff --git a/frontend/css/sections/admin/users.css.less b/frontend/css/sections/admin/users.css.less new file mode 100644 index 0000000..2d00953 --- /dev/null +++ b/frontend/css/sections/admin/users.css.less @@ -0,0 +1,22 @@ +.state-admin-user-list { + + table { + + .user_name{ + width: 250px; + } + + .email{ + width: 250px; + } + + .options { + width: 100px; + } + + .status{ + width: 50px; + } + } + +} diff --git a/frontend/css/sections/applications.css.less b/frontend/css/sections/applications.css.less new file mode 100644 index 0000000..16dddab --- /dev/null +++ b/frontend/css/sections/applications.css.less @@ -0,0 +1,20 @@ +.application-management { + .tab-content { + padding-top: 15px; + } + +} + + +.applications-list{ + table{ + + .resource_name{ + min-width: 250px; + } + + .options{ + min-width: 230px; + } + } +} diff --git a/frontend/css/sections/contacts.css.less b/frontend/css/sections/contacts.css.less new file mode 100644 index 0000000..f2e2c2a --- /dev/null +++ b/frontend/css/sections/contacts.css.less @@ -0,0 +1,14 @@ +.contacts { + #contact-holder, #contact-form { + font-size: 80%; + line-height: 150%; + h3 { + color: @color_orange; + } + } + + #message { + min-width: 390px; + min-height: 80px; + } +} diff --git a/frontend/css/sections/dashboard.css.less b/frontend/css/sections/dashboard.css.less new file mode 100644 index 0000000..cd96d58 --- /dev/null +++ b/frontend/css/sections/dashboard.css.less @@ -0,0 +1,16 @@ +.top-state-dashboard { + .agg-form { + .form-control { + width: 200px; + } + } + +} + +.state-dashboard-list { + table { + .options { + width: 250px; + } + } +} diff --git a/frontend/css/sections/events.css.less b/frontend/css/sections/events.css.less new file mode 100644 index 0000000..c09f944 --- /dev/null +++ b/frontend/css/sections/events.css.less @@ -0,0 +1,13 @@ +.top-state-events { + + .event-table { + .icons { + width: 50px; + vertical-align: middle + } + + .options { + width: 50px + } + } +} diff --git a/frontend/css/sections/features.css.less b/frontend/css/sections/features.css.less new file mode 100644 index 0000000..ffe28bd --- /dev/null +++ b/frontend/css/sections/features.css.less @@ -0,0 +1,48 @@ + +.features-section { + + #holder { + background-color: rgb(254, 254, 254) !important; + } + + h3, h2, h1 { + margin: 0px 0px 10px; + } + + h3 { + padding: 25px 25px; + background-color: #f6f6f6; + + } + + .feature-pane { + ul { + color: @color_secondary; + li { + margin: 20px 0px; + list-style: none; + .fa-square { + margin-right: 15px; + color: @color_orange; + } + } + } + } + + #menu { + .list-group-item { + color: @color_secondary; + font-weight: bold; + border-style: dashed; + background-color: #f6f6f6; + + &.active { + background-color: #e4e4e4; + border-style: solid; + border-color: #d4d4d4; + color: @color_vdark_grey; + } + } + } + +} diff --git a/frontend/css/sections/front_dashboard.css.less b/frontend/css/sections/front_dashboard.css.less new file mode 100644 index 0000000..f2a187b --- /dev/null +++ b/frontend/css/sections/front_dashboard.css.less @@ -0,0 +1,293 @@ +/* dashboard */ + +.top-state-front_dashboard { + + .tutorial { + .white-block; + } + + .pause_stream { + position: absolute; + top: 5px; + right: 50px; + } + + .limit_stream { + position: absolute; + top: 5px; + right: 10px; + } + + .combined-stat { + display: block; + min-width: 20%; + display: inline-block; + color: @color_white; + position: relative; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); + small { + font-size: 75%; + } + &:link, &:visited, &:hover { + color: @color_white !important; + } + + .fa-chevron-right { + opacity: 0.3; + position: absolute; + right: 5px; + top: 40px; + text-shadow: 0px 0px 0px; + } + + strong { + font-size: 160%; + display: inline-block; + margin: 5px 0; + } + + } + + #error-rate { + background-color: @color_red; + &:hover { + .transition-duration(); + background-color: lighten(@color_red, 10%); + } + } + + #satisfying-requests { + background-color: @color_green; + } + + #tolerated-requests { + background-color: @color_orange; + &:hover { + .transition-duration(); + background-color: lighten(@color_orange, 10%); + } + } + + #frustrating-requests { + background-color: @color_dark_orange; + &:hover { + .transition-duration(); + background-color: lighten(@color_dark_orange, 10%); + } + } + + #uptime-stats { + background-color: @color_med_blue; + &:hover { + .transition-duration(); + background-color: lighten(@color_med_blue, 10%); + } + } + + #server-container { + margin-bottom: 10px; + + .servers { + list-style: none; + margin: 0px; + } + + .frustrating { + .apdex { + color: @color_red; + } + + } + + .satisfactory { + .apdex { + color: @color_green; + } + } + + .tolerating { + .apdex { + color: @color_orange; + } + } + + } + + #assigned-container { + .white-block; + margin-top: 15px; + } + + #trending-container { + .white-block; + max-height: 800px; + overflow: auto; + margin-top: 15px; + } + + #report-timespan-picker-holder { + text-align: right; + } + + #graph-container { + .white-block; + margin: 0px 0px 10px 0px; + } + + #report_graph { + margin: auto; + border: 1px solid #B5BCC7; + width: 1008px; + overflow: hidden; + cursor: pointer; + min-height: 200px; + background-color: @color_white; + background-image: url('/static/appenlight/images/ajax_loader_3.gif'); + background-position: center center; + background-repeat: no-repeat; + } + + #report_graph:hover { + border: 1px solid #FF6F00; + -webkit-transition-duration: 0.5s; + -moz-transition-duration: 0.5s; + -o-transition-duration: 0.5s; + } + + #report_graph img { + -webkit-transition-duration: 0.5s; + -moz-transition-duration: 0.5s; + -o-transition-duration: 0.5s; + } + + #report_graph.toggle img { + margin-left: -1005px; + -webkit-transition-duration: 0.5s; + -moz-transition-duration: 0.5s; + -o-transition-duration: 0.5s; + } + + .point { + .border-radius(30px); + background-color: @color_orange; + font-size: 200%; + font-weight: bold; + padding: 5px 17px; + margin: 0px 10px 0px 0px; + vertical-align: middle; + color: @color_white; + float: left; + } + + #slow-statements { + table-layout: fixed; + width: 100%; + td { + padding: 0px 5px 10px 5px; + } + + .statement { + font-size: 80%; + padding: 10px 5px 0px 0px; + font-weight: bold; + } + + td.occurences { + width: 40px; + } + + span.occurences { + font-size: 86%; + font-weight: bold; + background-color: @color_orange; + border-radius: 3px; + color: @color_white; + display: inline-block; + min-height: 33px; + min-width: 33px; + padding: 8px 0 0; + text-align: center; + } + + .type { + font-size: 80%; + background-color: @color_grey; + padding: 2px 5px; + display: inline-block; + color: @color_white; + .border-radius(2px); + } + + .subtype { + font-size: 80%; + background-color: @color_orange; + padding: 2px 5px; + display: inline-block; + color: @color_white; + .border-radius(2px); + } + + .duration { + font-size: 80%; + background-color: @color_grey; + padding: 2px 5px; + display: inline-block; + color: @color_white; + .border-radius(2px); + margin: 0px 5px 0px 0px; + } + + .report-list { + font-size: 80%; + } + } + + #view-breakdown-container { + max-height: 472px; + overflow: auto; + + .report-list { + font-size: 86%; + } + + .view-name { + position: relative; + small { + font-weight: bold; + } + + } + + .stats { + margin-bottom: 5px; + } + + .bar { + border-radius: 5px; + box-shadow: 0px 0px 3px darken(@color_orange, 10%) inset; + height: 10px; + background-color: @color_orange; + } + + } + + #slow-statements-container, #slow-urls-container { + .white-block; + max-height: 800px; + overflow-y: auto; + table { + border: 0px; + } + margin-top: 15px; + } + + input.row-title { + min-width: 250px; + max-width: 400px; + display: inline-block; + } + + .chart-layout-form { + margin-bottom: 15px + } +} diff --git a/frontend/css/sections/index.css.less b/frontend/css/sections/index.css.less new file mode 100644 index 0000000..42b710f --- /dev/null +++ b/frontend/css/sections/index.css.less @@ -0,0 +1,213 @@ +.index-page { + + #holder { + box-shadow: 0px 0px 100px rgba(0, 0, 0, 0.05); + padding-top: 0px !important; + max-width: 1600px; + + } + + background-color: rgb(254, 254, 254); + + #top-navbar { + background-image: none !important; + + .pattern { + background-image: none !important; + } + + } + + .navbar-fixed-top { + position: absolute; + + .btn-orange { + margin-top: 35px + + } + } + + .nav > li > a { + margin-top: 35px; + color: @color_white; + &:link, &:visited { + color: @color_white + } + + } + + h1, h2, h3, h4 { + font-weight: bold; + color: @color_grey; + } + + #top-navbar { + background-color: transparent; + border: 0px; + min-height: 125px; + } + + #get-account-section { + min-height: 100px; + border-bottom: 2px solid @color_white; + border-top: 2px solid @color_white; + background-color: rgb(246, 246, 246); + box-shadow: 0px 0px 50px rgba(100, 100, 100, 0.3); + z-index: 5; + position: relative; + max-width: 1600px; + margin: 0px auto; + .col-xs-6 { + padding-top: 15px; + } + .col-xs-5 { + padding-top: 30px; + } + } + + #banner-section { + h1, h2, h3 { + color: @color_white; + } + position: relative; + color: @color_white; + background-image: url(/static/appenlight/images/sections/index/top_bg.jpg); + background-position: center top; + background-repeat: no-repeat; + + .pattern { + background-image: url(/static/appenlight/images/dots2_bg.png); + max-width: 1600px; + margin: 0px auto; + } + + .btn-outline { + border: 2px solid @color_white; + border-radius: 5px; + padding: 25px; + min-width: 275px; + font-size: 125%; + font-weight: bold; + text-transform: uppercase; + color: @color_white; + } + + .btn-green { + border-width: 2px; + border-radius: 5px; + padding: 25px; + min-width: 275px; + font-size: 125%; + font-weight: bold; + text-transform: uppercase; + color: @color_white; + background-color: rgba(147, 183, 21, 0.91) !important; + border-color: rgba(147, 183, 21, 0.91) !important; + margin-left: 10px; + } + + } + + #how-it-works-section { + .circle { + width: 140px; + height: 140px; + border-radius: 100px; + background-color: @color_orange; + display: inline-block; + margin-bottom: 20px; + } + + max-width: 1170px; + h3 { + font-size: 120%; + } + + padding-bottom: 50px; + p { + color: @color_secondary; + } + } + + #recommendations-section { + h2, h3 { + color: @color_white; + } + color: @color_white; + background-image: url(/static/appenlight/images/sections/index/recommendations.jpg); + background-position: center top; + min-height: 340px; + background-repeat: no-repeat; + + .pattern { + background-image: url(/static/appenlight/images/dots2_bg.png); + min-height: 340px; + max-width: 1600px; + margin: 0px auto; + } + + } + + #set-up-section { + h2, h3 { + color: @color_white; + } + color: @color_white; + background-image: url(/static/appenlight/images/sections/index/logos.jpg); + background-position: center top; + min-height: 270px; + background-repeat: no-repeat; + + .pattern { + background-image: url(/static/appenlight/images/dots2_bg.png); + min-height: 270px; + max-width: 1600px; + margin: 0px auto; + } + + margin-bottom: 70px; + + @media (max-width: 767px) { + img { + max-width: 135px; + } + } + + } + + #main-features { + padding: 60px 15px 0px 15px; + + @media (min-width: 1200px) { + .description { + padding-top: 55px; + } + } + + .description { + + text-align: justify; + p { + line-height: 150%; + font-size: 125%; + } + } + + h2 { + color: @color_orange; + font-size: 250%; + text-transform: uppercase; + } + + } + + #performance-monitoring, #uptime-monitoring { + background-color: #fbfbfb; + margin: 40px -15px; + padding: 40px 0px; + border-bottom: 3px solid #ececec; + border-top: 3px solid #ececec; + max-width: 1600px; + } + +} diff --git a/frontend/css/sections/integrations.css.less b/frontend/css/sections/integrations.css.less new file mode 100644 index 0000000..ffb0770 --- /dev/null +++ b/frontend/css/sections/integrations.css.less @@ -0,0 +1,11 @@ +.state-applications-integrations { + + .integration { + width: 100%; + margin-bottom: 10px; + white-space: normal; + min-height: 60px; + text-align: left; + } + +} diff --git a/frontend/css/sections/logs.css.less b/frontend/css/sections/logs.css.less new file mode 100644 index 0000000..a3537a2 --- /dev/null +++ b/frontend/css/sections/logs.css.less @@ -0,0 +1,103 @@ +table.log-list { + tr.odd, tr.even { + } + + .http_status { + width: 52px; + } + + .when { + width: 150px; + } + + .tick { + width: 20px !important; + } + + .c1 { + width: 130px; + + .app_name { + font-weight: bold; + } + .server { + color: #777777; + } + } + + .c2 { + .namespace { + color: @color_orange; + font-weight: bold; + } + word-break: break-all; + word-wrap: break-word; + } + + .odd .c3, .even .c3 { + width: 90px; + font-size: 85% + } + + .tag { + display: inline-block; + margin: 0 5px 0 0px; + padding: 2px 5px 1px; + font-size: 75%; + .border-radius(2px); + .inset-box-shadow(0, 0, 1px, rgba(0, 0, 0, 0.15)); + border: 0px solid @color_light_grey; + background-color: lighten(@color_light_grey, 10%); + letter-spacing: 0.5px; + color: darken(@color_light_grey, 33%); + .name { + text-transform: uppercase; + + } + } + .tag:hover { + .transition-duration; + -moz-transform: scale(1.20); + -webkit-transform: scale(1.20); + -o-transform: scale(1.20); + -ms-transform: scale(1.20); + transform: scale(1.20); + } + + .unknown { + color: @color_grey !important; + } + + .debug { + background-color: @color_green !important; + color: #ffffff !important; + } + + .info { + background-color: lighten(@color_med_blue, 65%) !important; + color: @color_med_blue !important; + border-color: lighten(@color_med_blue, 33%); + } + + .warning { + background-color: lighten(@color_orange, 55%) !important; + color: @color_orange !important; + border-color: lighten(@color_orange, 33%); + } + + .error { + background-color: lighten(@color_red, 60%) !important; + color: @color_red !important; + border-color: lighten(@color_red, 50%); + } + .critical { + background-color: #000000 !important; + color: #ffffff !important; + background-color: #000000; + } + .log { + font-size: 85%; + margin-top: 5px; + } + +} diff --git a/frontend/css/sections/pages.css.less b/frontend/css/sections/pages.css.less new file mode 100644 index 0000000..c630975 --- /dev/null +++ b/frontend/css/sections/pages.css.less @@ -0,0 +1,145 @@ +.pages-section { + + .container { + width: 100%; + max-width: 1600px; + } + + .doc-menu { + padding: 0px; + .white-block; + li { + line-height: 2em; + } + } + + .doc-sub-menu { + padding-left: 15px; + } + + .doc-menu li { + list-style-type: none; + } + + table { + margin: 5px 0px 5px 0px; + width: 100%; + border-collapse: collapse; + + caption { + color: @color_grey; + padding: 5px; + font-weight: bold; + text-transform: uppercase; + text-align: center; + + } + + caption a:link, table caption a:visited { + color: #ffffff; + text-decoration: none; + font-weight: bold; + } + + caption a:link, table caption a:hover { + color: #ffcc00; + text-decoration: none; + font-weight: bold; + } + + thead { + background-color: #ffffff; + } + + thead tr { + .inset-box-shadow(0, 1px, 1px, @color: @color_white); + background: rgb(245, 245, 245); + + text-transform: uppercase; + + } + + tr.header td, thead th { + color: @color_grey; + .box-shadow(1px 0px 1px #ffffff inset); + padding: 5px 10px; + a:link, a:visited { + color: @color_dark_grey; + font-weight: bold; + display: block; + } + position: relative; + + } + + td, th { + padding: 25px 15px; + border-bottom: 1px solid @color_light_grey; + vertical-align: top; + } + + tr.odd td { + background-color: #FAFAFA; + } + + tr.even td { + background-color: #ffffff; + } + + .no { + width: 30px; + } + + td.ordering.dsc, td.ordering.asc { + padding-right: 20px; + .transition-duration; + /* position: relative; */ + } + + td.ordering { + .marker { + display: block; + float: right; + height: 10px; + margin: -13px -15px 0px 0; + width: 10px; + background-repeat: no-repeat; + + } + &.asc .marker { + background-image: url("/static/appenlight/images/dark_asc.png"); + + } + &.dsc .marker { + background-image: url("/static/appenlight/images/dark_dsc.png"); + + } + a:link, a:visited { + color: @color_vdark_grey !important; + font-weight: bold; + text-decoration: underline; + } + } + + } + +} + +.api-key-info { + .key-name { + font-size: 120%; + } + .key-req { + display: block; + color: lighten(@color_grey, 15%); + font-size: 80%; + } + + .docs-section { + p, ul { + line-height: 150%; + margin-bottom: 25px; + } + } + +} diff --git a/frontend/css/sections/register.css.less b/frontend/css/sections/register.css.less new file mode 100644 index 0000000..f3f4b16 --- /dev/null +++ b/frontend/css/sections/register.css.less @@ -0,0 +1,67 @@ +.register-section { + + #social_block { + h2 { + margin-bottom: 30px; + } + + margin-bottom: 20px; + } + + #social_providers { + list-style: none; + margin: 20px 0px; + padding: 0px; + li { + display: inline-block; + margin: 0px 10px; + } + } + + #row-email .description { + font-size: 70%; + } + + #social-form .btn { + color: #ffffff; + margin-bottom: 10px; + text-align: left; + font-weight: bold; + text-transform: uppercase; + border: 1px solid #151515; + } + + #social-form span { + vertical-align: middle; + min-width: 30px; + } + + #btn-google { + background-color: #BB2A2A !important; + border: 1px solid #8C0202 !important; + } + + #btn-twitter { + background-color: #10B8F8 !important; + border: 1px solid #0195CF !important; + } + + #btn-bitbucket { + background-color: #205081 !important; + border: 1px solid #1D4A78 !important; + } + + #btn-github { + background-color: #333333 !important; + border: 1px solid #222222 !important; + } + + #btn-bitbucket i { + font-size: 150%; + } + + #btn-demo { + border: 1px solid #239100 !important + } + +} diff --git a/frontend/css/sections/reports.css.less b/frontend/css/sections/reports.css.less new file mode 100644 index 0000000..515b079 --- /dev/null +++ b/frontend/css/sections/reports.css.less @@ -0,0 +1,427 @@ +.top-state-report { + + .affected-user-list { + margin: 0px; + padding: 0px; + li { + list-style: none; + padding-bottom: 5px; + } + } + + .inspect_vars .glyphicon { + margin: 0 2px; + padding: 2px 5px 5px; + } + + .priority { + border-radius: 20px 20px 20px 20px; + display: inline-block; + font-size: 79.4%; + font-weight: bold; + height: 15px; + padding: 1px 0 0; + text-align: center; + width: 15px; + position: absolute; + margin-left: 22px; + margin-top: -7px; + box-shadow: 0px 0px 2px rgb(255, 255, 255); + } + + .priority-1 { + background-color: rgb(255, 233, 233); + color: rgb(255, 123, 123); + .priority + } + + .priority-2 { + background-color: rgb(255, 211, 211); + color: @color_white; + .priority + } + + .priority-3 { + background-color: rgb(255, 189, 189); + color: @color_white; + .priority + } + + .priority-4 { + background-color: rgb(255, 167, 167); + color: @color_white; + .priority + } + + .priority-5 { + background-color: rgb(255, 145, 145); + color: @color_white; + .priority + } + + .priority-6 { + background-color: rgb(255, 123, 123); + color: @color_white; + .priority + } + + .priority-7 { + background-color: rgb(255, 101, 101); + color: @color_white; + .priority + } + + .priority-8 { + background-color: rgb(255, 79, 79); + color: @color_white; + .priority + } + + .priority-9 { + background-color: rgb(255, 57, 57); + color: @color_white; + .priority + } + + .priority-10 { + background-color: rgb(255, 35, 35); + color: @color_white; + .priority + } + + .table.report-list { + table-layout: fixed; + + .occurences { + width: 45px; + } + + .priority { + width: 45px; + } + + .when { + width: 120px; + } + + .report { + font-weight: bold; + } + + tbody td { + padding: 15px; + } + + .occurences { + .count { + background-color: @color_orange; + border-radius: 3px; + color: #FFFFFF; + display: inline-block; + font-weight: bold; + height: 33px; + padding: 7px 0 0; + text-align: center; + width: 33px; + } + + .fixed { + background-color: @color_green; + } + + .public { + background-color: @color_dark_blue; + } + + .reviewed { + background-color: @color_light_grey; + color: #000000; + } + + } + .when { + font-size: 80%; + } + .url { + color: #777777; + font-weight: normal; + } + + .tick { + width: 20px !important; + } + + .application { + width: 150px; + } + + td.application { + font-size: 80%; + .app_name { + font-weight: bold; + } + .server { + color: #777777; + } + } + + } + + #error-history_pane div { + } + + #request-details-container #request-details { + font-size: 80%; + > ul { + padding: 0px; + } + li { + margin: 0px 0px 10px 0px; + list-style: none; + } + } + + .reformat { + .white-block; + pre { + font-size: 0.88em; + font-family: monospace, monospace; + line-height: 1.8em; + color: #555555; + + .border-radius(0px); + border: 0px; + margin: 0px; + } + } + + .report-table { + font-size: 86%; + width: 100%; + td.table-label { + width: 115px; + } + &.with-ellipsis { + table-layout: fixed; + } + + &.with-tags { + td.table-label { + width: 150px; + .word-wrap; + } + } + } + + .report-table td.data { + padding-bottom: 10px; + vertical-align: top; + } + + .report-table td.table-label { + min-width: 115px; + text-align: left; + padding: 0px 10px 0px 0px; + font-weight: bold; + line-height: 1.5em; + vertical-align: top; + } + .userRepresentation { + min-width: 200px; + height: 55px; + overflow: hidden; + margin-bottom: 5px; + list-style: none; + padding: 5px; + -webkit-transition-duration: 0.25s; + -moz-transition-duration: 0.25s; + -o-transition-duration: 0.25s; + transition-duration: 0.25s; + + p { + margin: 0px; + + } + + &:hover { + background-color: rgb(255, 111, 0); + -webkit-transition-duration: 0.25s; + -moz-transition-duration: 0.25s; + -o-transition-duration: 0.25s; + transition-duration: 0.25s; + } + + img { + float: left; + height: 50px; + max-width: 50px; + margin: 0px 5px 0px 0px; + } + + } + .user_picker { + font-size: 80%; + + .column { + width: 33%; + float: left; + } + + ul { + padding: 0px; + height: 400px; + overflow: auto; + min-width: 250px; + border: 1px solid #eeeeee; + padding: 5px; + margin: 5px 0px; + } + } + + .comment-holder { + border: 1px solid @color_light_grey; + } + + .comment-holder { + background-color: #FFFFFF; + border: 1px solid #E1E1E1; + margin-bottom: 5px; + padding: 10px; + } + + .comment-holder .icon.big { + margin: 0 0 0 -7px; + } + + #comment_create .TextAreaField { + width: 90%; + height: 70px; + } + + #basic-info_pane { + position: relative; + } + + .switch_detail_holder { + position: absolute; + right: 10px; + font-size: 70%; + } + + #similar-reports { + .notice { + color: lighten(@color_grey, 20%); + } + } + + .frames { + margin-top: 15px; + + .white-block; + + .frame { + padding-bottom: 10px; + .icon { + padding: 10px + } + &.even { + background-color: #f9f9f9; + } + } + + .frameline, .cline { + line-height: 150%; + color: lighten(@color_grey, 40%); + } + .mono { + font-family: monospace; + white-space: pre-wrap; + font-size: 86%; + } + + .dim:hover { + opacity: 0.8; + } + + .file { + color: @color_med_blue; + font-weight: bold; + } + .cline { + padding-left: 50px; + color: @color_vdark_grey; + } + .line { + color: @color_dark_blue; + font-weight: bold; + } + .fn { + color: @color_dark_orange; + font-weight: bold; + } + + } + + .var-listing { + td { + padding: 10px 10px 10px 10px; + } + .var-label { + margin-right: 10px; + font-weight: bold; + text-align: right; + vertical-align: top; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + } + + .odd { + background-color: rgba(0, 0, 0, 0.024); + } + + } + + .report-section { + #row-start_date, #row-end_date, #row-priority, #row-http_status { + display: inline-block; + width: 110px + } + + .no-vars { + width: 31px; + display: inline-block; + } + + } + + .slow-report-section { + #row-start_date, #row-end_date { + display: inline-block; + width: 110px + } + + #row-priority { + display: inline-block; + width: 75px + } + + #row-min_request_time { + display: inline-block; + width: 140px; + input { + width: 75px; + } + } + + } + + .fixed { + color: @color_red; + } + + .not-fixed { + color: @color_green; + } + +} diff --git a/frontend/css/sections/slow_reports.css.less b/frontend/css/sections/slow_reports.css.less new file mode 100644 index 0000000..002ee51 --- /dev/null +++ b/frontend/css/sections/slow_reports.css.less @@ -0,0 +1,29 @@ +.state-report-list_slow { + table.report-list { + + .occurences { + width: 45px; + } + + .report { + font-weight: bold; + } + + .status { + width: 30px; + } + + .average_duration { + width: 85px; + font-weight: bold; + } + + .when { + width: 120px; + } + + .tick { + width: 20px !important; + } + } +} diff --git a/frontend/css/sections/uptime.css.less b/frontend/css/sections/uptime.css.less new file mode 100644 index 0000000..09c8e07 --- /dev/null +++ b/frontend/css/sections/uptime.css.less @@ -0,0 +1,36 @@ +.top-state-uptime { + + table { + + .problem { + color: @color_red; + font-weight: bold; + td { + background-color: #FFE2E2 !important; + } + } + + .interval { + width: 150px; + } + .retries { + width: 90px; + text-align: center; + } + .http_status { + width: 150px; + text-align: center; + } + .location { + width: 200px; + text-align: center; + } + } + + .uptime-list { + table { + border: 0 !important; + } + } + +} diff --git a/frontend/css/sections/user.css.less b/frontend/css/sections/user.css.less new file mode 100644 index 0000000..e1e2404 --- /dev/null +++ b/frontend/css/sections/user.css.less @@ -0,0 +1,11 @@ +.state-user-profile-auth_tokens { + .table { + .expires, .created { + width: 130px; + } + + .options { + width: 80px; + } + } +} diff --git a/frontend/css/vendors/bootstrap customizations.txt b/frontend/css/vendors/bootstrap customizations.txt new file mode 100644 index 0000000..dcb2f9e --- /dev/null +++ b/frontend/css/vendors/bootstrap customizations.txt @@ -0,0 +1,43 @@ +// variables.less + +@grid-gutter-width: 10px; + +// CHANGED BY AE + +// @media (max-width: @grid-float-breakpoint-max) { +// // Dropdowns get custom display when collapsed +// .open .dropdown-menu { +// position: static; +// float: none; +// width: auto; +// margin-top: 0; +// background-color: transparent; +// border: 0; +// box-shadow: none; +// > li > a, +// .dropdown-header { +// padding: 5px 15px 5px 25px; +// } +// > li > a { +// line-height: @line-height-computed; +// &:hover, +// &:focus { +// background-image: none; +// } +// } +// } +// } + +// // Uncollapse the nav +// @media (min-width: @grid-float-breakpoint) { +// float: left; +// margin: 0; +// +// > li { +// float: left; +// > a { +// padding-top: @navbar-padding-vertical; +// padding-bottom: @navbar-padding-vertical; +// } +// } +// } \ No newline at end of file diff --git a/frontend/css/vendors/c3.css b/frontend/css/vendors/c3.css new file mode 100644 index 0000000..ab0b70a --- /dev/null +++ b/frontend/css/vendors/c3.css @@ -0,0 +1,158 @@ +/*-- Chart --*/ +.c3 svg { + font: 10px sans-serif; } + +.c3 path, .c3 line { + fill: none; + stroke: #000; } + +.c3 text { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; } + +.c3-legend-item-tile, .c3-xgrid-focus, .c3-ygrid, .c3-event-rect, .c3-bars path { + shape-rendering: crispEdges; } + +.c3-chart-arc path { + stroke: #fff; } + +.c3-chart-arc text { + fill: #fff; + font-size: 13px; } + +/*-- Axis --*/ +/*-- Grid --*/ +.c3-grid line { + stroke: #aaa; } + +.c3-grid text { + fill: #aaa; } + +.c3-xgrid, .c3-ygrid { + stroke-dasharray: 3 3; } + +/*-- Text on Chart --*/ +.c3-text.c3-empty { + fill: #808080; + font-size: 2em; } + +/*-- Line --*/ +.c3-line { + stroke-width: 1px; } + +/*-- Point --*/ +.c3-circle._expanded_ { + stroke-width: 1px; + stroke: white; } + +.c3-selected-circle { + fill: white; + stroke-width: 2px; } + +/*-- Bar --*/ +.c3-bar { + stroke-width: 0; } + +.c3-bar._expanded_ { + fill-opacity: 0.75; } + +/*-- Focus --*/ +.c3-target.c3-focused { + opacity: 1; } + +.c3-target.c3-focused path.c3-line, .c3-target.c3-focused path.c3-step { + stroke-width: 2px; } + +.c3-target.c3-defocused { + opacity: 0.3 !important; } + +/*-- Region --*/ +.c3-region { + fill: steelblue; + fill-opacity: 0.1; } + +/*-- Brush --*/ +.c3-brush .extent { + fill-opacity: 0.1; } + +/*-- Select - Drag --*/ +/*-- Legend --*/ +.c3-legend-item { + font-size: 12px; } + +.c3-legend-item-hidden { + opacity: 0.15; } + +.c3-legend-background { + opacity: 0.75; + fill: white; + stroke: lightgray; + stroke-width: 1; } + +/*-- Tooltip --*/ +.c3-tooltip-container { + z-index: 10; } + +.c3-tooltip { + border-collapse: collapse; + border-spacing: 0; + background-color: #fff; + empty-cells: show; + -webkit-box-shadow: 7px 7px 12px -9px #777777; + -moz-box-shadow: 7px 7px 12px -9px #777777; + box-shadow: 7px 7px 12px -9px #777777; + opacity: 0.9; } + +.c3-tooltip tr { + border: 1px solid #CCC; } + +.c3-tooltip th { + background-color: #aaa; + font-size: 14px; + padding: 2px 5px; + text-align: left; + color: #FFF; } + +.c3-tooltip td { + font-size: 13px; + padding: 3px 6px; + background-color: #fff; + border-left: 1px dotted #999; } + +.c3-tooltip td > span { + display: inline-block; + width: 10px; + height: 10px; + margin-right: 6px; } + +.c3-tooltip td.value { + text-align: right; } + +/*-- Area --*/ +.c3-area { + stroke-width: 0; + opacity: 0.2; } + +/*-- Arc --*/ +.c3-chart-arcs-title { + dominant-baseline: middle; + font-size: 1.3em; } + +.c3-chart-arcs .c3-chart-arcs-background { + fill: #e0e0e0; + stroke: none; } + +.c3-chart-arcs .c3-chart-arcs-gauge-unit { + fill: #000; + font-size: 16px; } + +.c3-chart-arcs .c3-chart-arcs-gauge-max { + fill: #777; } + +.c3-chart-arcs .c3-chart-arcs-gauge-min { + fill: #777; } + +.c3-chart-arc .c3-gauge-value { + fill: #000; + /* font-size: 28px !important;*/ } diff --git a/frontend/css/vendors/font-awesome.css b/frontend/css/vendors/font-awesome.css new file mode 100644 index 0000000..2dcdc22 --- /dev/null +++ b/frontend/css/vendors/font-awesome.css @@ -0,0 +1,1801 @@ +/*! + * Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ +/* FONT PATH + * -------------------------- */ +@font-face { + font-family: 'FontAwesome'; + src: url('../fonts/fontawesome-webfont.eot?v=4.3.0'); + src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.3.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.3.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.3.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.3.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.3.0#fontawesomeregular') format('svg'); + font-weight: normal; + font-style: normal; +} +.fa { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + transform: translate(0, 0); +} +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.33333333em; + line-height: 0.75em; + vertical-align: -15%; +} +.fa-2x { + font-size: 2em; +} +.fa-3x { + font-size: 3em; +} +.fa-4x { + font-size: 4em; +} +.fa-5x { + font-size: 5em; +} +.fa-fw { + width: 1.28571429em; + text-align: center; +} +.fa-ul { + padding-left: 0; + margin-left: 2.14285714em; + list-style-type: none; +} +.fa-ul > li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.85714286em; +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: .3em; +} +.fa.pull-right { + margin-left: .3em; +} +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none; +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} +.fa-music:before { + content: "\f001"; +} +.fa-search:before { + content: "\f002"; +} +.fa-envelope-o:before { + content: "\f003"; +} +.fa-heart:before { + content: "\f004"; +} +.fa-star:before { + content: "\f005"; +} +.fa-star-o:before { + content: "\f006"; +} +.fa-user:before { + content: "\f007"; +} +.fa-film:before { + content: "\f008"; +} +.fa-th-large:before { + content: "\f009"; +} +.fa-th:before { + content: "\f00a"; +} +.fa-th-list:before { + content: "\f00b"; +} +.fa-check:before { + content: "\f00c"; +} +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: "\f00d"; +} +.fa-search-plus:before { + content: "\f00e"; +} +.fa-search-minus:before { + content: "\f010"; +} +.fa-power-off:before { + content: "\f011"; +} +.fa-signal:before { + content: "\f012"; +} +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} +.fa-trash-o:before { + content: "\f014"; +} +.fa-home:before { + content: "\f015"; +} +.fa-file-o:before { + content: "\f016"; +} +.fa-clock-o:before { + content: "\f017"; +} +.fa-road:before { + content: "\f018"; +} +.fa-download:before { + content: "\f019"; +} +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} +.fa-inbox:before { + content: "\f01c"; +} +.fa-play-circle-o:before { + content: "\f01d"; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} +.fa-refresh:before { + content: "\f021"; +} +.fa-list-alt:before { + content: "\f022"; +} +.fa-lock:before { + content: "\f023"; +} +.fa-flag:before { + content: "\f024"; +} +.fa-headphones:before { + content: "\f025"; +} +.fa-volume-off:before { + content: "\f026"; +} +.fa-volume-down:before { + content: "\f027"; +} +.fa-volume-up:before { + content: "\f028"; +} +.fa-qrcode:before { + content: "\f029"; +} +.fa-barcode:before { + content: "\f02a"; +} +.fa-tag:before { + content: "\f02b"; +} +.fa-tags:before { + content: "\f02c"; +} +.fa-book:before { + content: "\f02d"; +} +.fa-bookmark:before { + content: "\f02e"; +} +.fa-print:before { + content: "\f02f"; +} +.fa-camera:before { + content: "\f030"; +} +.fa-font:before { + content: "\f031"; +} +.fa-bold:before { + content: "\f032"; +} +.fa-italic:before { + content: "\f033"; +} +.fa-text-height:before { + content: "\f034"; +} +.fa-text-width:before { + content: "\f035"; +} +.fa-align-left:before { + content: "\f036"; +} +.fa-align-center:before { + content: "\f037"; +} +.fa-align-right:before { + content: "\f038"; +} +.fa-align-justify:before { + content: "\f039"; +} +.fa-list:before { + content: "\f03a"; +} +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} +.fa-indent:before { + content: "\f03c"; +} +.fa-video-camera:before { + content: "\f03d"; +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "\f03e"; +} +.fa-pencil:before { + content: "\f040"; +} +.fa-map-marker:before { + content: "\f041"; +} +.fa-adjust:before { + content: "\f042"; +} +.fa-tint:before { + content: "\f043"; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} +.fa-share-square-o:before { + content: "\f045"; +} +.fa-check-square-o:before { + content: "\f046"; +} +.fa-arrows:before { + content: "\f047"; +} +.fa-step-backward:before { + content: "\f048"; +} +.fa-fast-backward:before { + content: "\f049"; +} +.fa-backward:before { + content: "\f04a"; +} +.fa-play:before { + content: "\f04b"; +} +.fa-pause:before { + content: "\f04c"; +} +.fa-stop:before { + content: "\f04d"; +} +.fa-forward:before { + content: "\f04e"; +} +.fa-fast-forward:before { + content: "\f050"; +} +.fa-step-forward:before { + content: "\f051"; +} +.fa-eject:before { + content: "\f052"; +} +.fa-chevron-left:before { + content: "\f053"; +} +.fa-chevron-right:before { + content: "\f054"; +} +.fa-plus-circle:before { + content: "\f055"; +} +.fa-minus-circle:before { + content: "\f056"; +} +.fa-times-circle:before { + content: "\f057"; +} +.fa-check-circle:before { + content: "\f058"; +} +.fa-question-circle:before { + content: "\f059"; +} +.fa-info-circle:before { + content: "\f05a"; +} +.fa-crosshairs:before { + content: "\f05b"; +} +.fa-times-circle-o:before { + content: "\f05c"; +} +.fa-check-circle-o:before { + content: "\f05d"; +} +.fa-ban:before { + content: "\f05e"; +} +.fa-arrow-left:before { + content: "\f060"; +} +.fa-arrow-right:before { + content: "\f061"; +} +.fa-arrow-up:before { + content: "\f062"; +} +.fa-arrow-down:before { + content: "\f063"; +} +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} +.fa-expand:before { + content: "\f065"; +} +.fa-compress:before { + content: "\f066"; +} +.fa-plus:before { + content: "\f067"; +} +.fa-minus:before { + content: "\f068"; +} +.fa-asterisk:before { + content: "\f069"; +} +.fa-exclamation-circle:before { + content: "\f06a"; +} +.fa-gift:before { + content: "\f06b"; +} +.fa-leaf:before { + content: "\f06c"; +} +.fa-fire:before { + content: "\f06d"; +} +.fa-eye:before { + content: "\f06e"; +} +.fa-eye-slash:before { + content: "\f070"; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} +.fa-plane:before { + content: "\f072"; +} +.fa-calendar:before { + content: "\f073"; +} +.fa-random:before { + content: "\f074"; +} +.fa-comment:before { + content: "\f075"; +} +.fa-magnet:before { + content: "\f076"; +} +.fa-chevron-up:before { + content: "\f077"; +} +.fa-chevron-down:before { + content: "\f078"; +} +.fa-retweet:before { + content: "\f079"; +} +.fa-shopping-cart:before { + content: "\f07a"; +} +.fa-folder:before { + content: "\f07b"; +} +.fa-folder-open:before { + content: "\f07c"; +} +.fa-arrows-v:before { + content: "\f07d"; +} +.fa-arrows-h:before { + content: "\f07e"; +} +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: "\f080"; +} +.fa-twitter-square:before { + content: "\f081"; +} +.fa-facebook-square:before { + content: "\f082"; +} +.fa-camera-retro:before { + content: "\f083"; +} +.fa-key:before { + content: "\f084"; +} +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} +.fa-comments:before { + content: "\f086"; +} +.fa-thumbs-o-up:before { + content: "\f087"; +} +.fa-thumbs-o-down:before { + content: "\f088"; +} +.fa-star-half:before { + content: "\f089"; +} +.fa-heart-o:before { + content: "\f08a"; +} +.fa-sign-out:before { + content: "\f08b"; +} +.fa-linkedin-square:before { + content: "\f08c"; +} +.fa-thumb-tack:before { + content: "\f08d"; +} +.fa-external-link:before { + content: "\f08e"; +} +.fa-sign-in:before { + content: "\f090"; +} +.fa-trophy:before { + content: "\f091"; +} +.fa-github-square:before { + content: "\f092"; +} +.fa-upload:before { + content: "\f093"; +} +.fa-lemon-o:before { + content: "\f094"; +} +.fa-phone:before { + content: "\f095"; +} +.fa-square-o:before { + content: "\f096"; +} +.fa-bookmark-o:before { + content: "\f097"; +} +.fa-phone-square:before { + content: "\f098"; +} +.fa-twitter:before { + content: "\f099"; +} +.fa-facebook-f:before, +.fa-facebook:before { + content: "\f09a"; +} +.fa-github:before { + content: "\f09b"; +} +.fa-unlock:before { + content: "\f09c"; +} +.fa-credit-card:before { + content: "\f09d"; +} +.fa-rss:before { + content: "\f09e"; +} +.fa-hdd-o:before { + content: "\f0a0"; +} +.fa-bullhorn:before { + content: "\f0a1"; +} +.fa-bell:before { + content: "\f0f3"; +} +.fa-certificate:before { + content: "\f0a3"; +} +.fa-hand-o-right:before { + content: "\f0a4"; +} +.fa-hand-o-left:before { + content: "\f0a5"; +} +.fa-hand-o-up:before { + content: "\f0a6"; +} +.fa-hand-o-down:before { + content: "\f0a7"; +} +.fa-arrow-circle-left:before { + content: "\f0a8"; +} +.fa-arrow-circle-right:before { + content: "\f0a9"; +} +.fa-arrow-circle-up:before { + content: "\f0aa"; +} +.fa-arrow-circle-down:before { + content: "\f0ab"; +} +.fa-globe:before { + content: "\f0ac"; +} +.fa-wrench:before { + content: "\f0ad"; +} +.fa-tasks:before { + content: "\f0ae"; +} +.fa-filter:before { + content: "\f0b0"; +} +.fa-briefcase:before { + content: "\f0b1"; +} +.fa-arrows-alt:before { + content: "\f0b2"; +} +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} +.fa-cloud:before { + content: "\f0c2"; +} +.fa-flask:before { + content: "\f0c3"; +} +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} +.fa-paperclip:before { + content: "\f0c6"; +} +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} +.fa-square:before { + content: "\f0c8"; +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "\f0c9"; +} +.fa-list-ul:before { + content: "\f0ca"; +} +.fa-list-ol:before { + content: "\f0cb"; +} +.fa-strikethrough:before { + content: "\f0cc"; +} +.fa-underline:before { + content: "\f0cd"; +} +.fa-table:before { + content: "\f0ce"; +} +.fa-magic:before { + content: "\f0d0"; +} +.fa-truck:before { + content: "\f0d1"; +} +.fa-pinterest:before { + content: "\f0d2"; +} +.fa-pinterest-square:before { + content: "\f0d3"; +} +.fa-google-plus-square:before { + content: "\f0d4"; +} +.fa-google-plus:before { + content: "\f0d5"; +} +.fa-money:before { + content: "\f0d6"; +} +.fa-caret-down:before { + content: "\f0d7"; +} +.fa-caret-up:before { + content: "\f0d8"; +} +.fa-caret-left:before { + content: "\f0d9"; +} +.fa-caret-right:before { + content: "\f0da"; +} +.fa-columns:before { + content: "\f0db"; +} +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: "\f0dd"; +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: "\f0de"; +} +.fa-envelope:before { + content: "\f0e0"; +} +.fa-linkedin:before { + content: "\f0e1"; +} +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} +.fa-comment-o:before { + content: "\f0e5"; +} +.fa-comments-o:before { + content: "\f0e6"; +} +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} +.fa-sitemap:before { + content: "\f0e8"; +} +.fa-umbrella:before { + content: "\f0e9"; +} +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} +.fa-lightbulb-o:before { + content: "\f0eb"; +} +.fa-exchange:before { + content: "\f0ec"; +} +.fa-cloud-download:before { + content: "\f0ed"; +} +.fa-cloud-upload:before { + content: "\f0ee"; +} +.fa-user-md:before { + content: "\f0f0"; +} +.fa-stethoscope:before { + content: "\f0f1"; +} +.fa-suitcase:before { + content: "\f0f2"; +} +.fa-bell-o:before { + content: "\f0a2"; +} +.fa-coffee:before { + content: "\f0f4"; +} +.fa-cutlery:before { + content: "\f0f5"; +} +.fa-file-text-o:before { + content: "\f0f6"; +} +.fa-building-o:before { + content: "\f0f7"; +} +.fa-hospital-o:before { + content: "\f0f8"; +} +.fa-ambulance:before { + content: "\f0f9"; +} +.fa-medkit:before { + content: "\f0fa"; +} +.fa-fighter-jet:before { + content: "\f0fb"; +} +.fa-beer:before { + content: "\f0fc"; +} +.fa-h-square:before { + content: "\f0fd"; +} +.fa-plus-square:before { + content: "\f0fe"; +} +.fa-angle-double-left:before { + content: "\f100"; +} +.fa-angle-double-right:before { + content: "\f101"; +} +.fa-angle-double-up:before { + content: "\f102"; +} +.fa-angle-double-down:before { + content: "\f103"; +} +.fa-angle-left:before { + content: "\f104"; +} +.fa-angle-right:before { + content: "\f105"; +} +.fa-angle-up:before { + content: "\f106"; +} +.fa-angle-down:before { + content: "\f107"; +} +.fa-desktop:before { + content: "\f108"; +} +.fa-laptop:before { + content: "\f109"; +} +.fa-tablet:before { + content: "\f10a"; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} +.fa-circle-o:before { + content: "\f10c"; +} +.fa-quote-left:before { + content: "\f10d"; +} +.fa-quote-right:before { + content: "\f10e"; +} +.fa-spinner:before { + content: "\f110"; +} +.fa-circle:before { + content: "\f111"; +} +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} +.fa-github-alt:before { + content: "\f113"; +} +.fa-folder-o:before { + content: "\f114"; +} +.fa-folder-open-o:before { + content: "\f115"; +} +.fa-smile-o:before { + content: "\f118"; +} +.fa-frown-o:before { + content: "\f119"; +} +.fa-meh-o:before { + content: "\f11a"; +} +.fa-gamepad:before { + content: "\f11b"; +} +.fa-keyboard-o:before { + content: "\f11c"; +} +.fa-flag-o:before { + content: "\f11d"; +} +.fa-flag-checkered:before { + content: "\f11e"; +} +.fa-terminal:before { + content: "\f120"; +} +.fa-code:before { + content: "\f121"; +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} +.fa-location-arrow:before { + content: "\f124"; +} +.fa-crop:before { + content: "\f125"; +} +.fa-code-fork:before { + content: "\f126"; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} +.fa-question:before { + content: "\f128"; +} +.fa-info:before { + content: "\f129"; +} +.fa-exclamation:before { + content: "\f12a"; +} +.fa-superscript:before { + content: "\f12b"; +} +.fa-subscript:before { + content: "\f12c"; +} +.fa-eraser:before { + content: "\f12d"; +} +.fa-puzzle-piece:before { + content: "\f12e"; +} +.fa-microphone:before { + content: "\f130"; +} +.fa-microphone-slash:before { + content: "\f131"; +} +.fa-shield:before { + content: "\f132"; +} +.fa-calendar-o:before { + content: "\f133"; +} +.fa-fire-extinguisher:before { + content: "\f134"; +} +.fa-rocket:before { + content: "\f135"; +} +.fa-maxcdn:before { + content: "\f136"; +} +.fa-chevron-circle-left:before { + content: "\f137"; +} +.fa-chevron-circle-right:before { + content: "\f138"; +} +.fa-chevron-circle-up:before { + content: "\f139"; +} +.fa-chevron-circle-down:before { + content: "\f13a"; +} +.fa-html5:before { + content: "\f13b"; +} +.fa-css3:before { + content: "\f13c"; +} +.fa-anchor:before { + content: "\f13d"; +} +.fa-unlock-alt:before { + content: "\f13e"; +} +.fa-bullseye:before { + content: "\f140"; +} +.fa-ellipsis-h:before { + content: "\f141"; +} +.fa-ellipsis-v:before { + content: "\f142"; +} +.fa-rss-square:before { + content: "\f143"; +} +.fa-play-circle:before { + content: "\f144"; +} +.fa-ticket:before { + content: "\f145"; +} +.fa-minus-square:before { + content: "\f146"; +} +.fa-minus-square-o:before { + content: "\f147"; +} +.fa-level-up:before { + content: "\f148"; +} +.fa-level-down:before { + content: "\f149"; +} +.fa-check-square:before { + content: "\f14a"; +} +.fa-pencil-square:before { + content: "\f14b"; +} +.fa-external-link-square:before { + content: "\f14c"; +} +.fa-share-square:before { + content: "\f14d"; +} +.fa-compass:before { + content: "\f14e"; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} +.fa-gbp:before { + content: "\f154"; +} +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} +.fa-file:before { + content: "\f15b"; +} +.fa-file-text:before { + content: "\f15c"; +} +.fa-sort-alpha-asc:before { + content: "\f15d"; +} +.fa-sort-alpha-desc:before { + content: "\f15e"; +} +.fa-sort-amount-asc:before { + content: "\f160"; +} +.fa-sort-amount-desc:before { + content: "\f161"; +} +.fa-sort-numeric-asc:before { + content: "\f162"; +} +.fa-sort-numeric-desc:before { + content: "\f163"; +} +.fa-thumbs-up:before { + content: "\f164"; +} +.fa-thumbs-down:before { + content: "\f165"; +} +.fa-youtube-square:before { + content: "\f166"; +} +.fa-youtube:before { + content: "\f167"; +} +.fa-xing:before { + content: "\f168"; +} +.fa-xing-square:before { + content: "\f169"; +} +.fa-youtube-play:before { + content: "\f16a"; +} +.fa-dropbox:before { + content: "\f16b"; +} +.fa-stack-overflow:before { + content: "\f16c"; +} +.fa-instagram:before { + content: "\f16d"; +} +.fa-flickr:before { + content: "\f16e"; +} +.fa-adn:before { + content: "\f170"; +} +.fa-bitbucket:before { + content: "\f171"; +} +.fa-bitbucket-square:before { + content: "\f172"; +} +.fa-tumblr:before { + content: "\f173"; +} +.fa-tumblr-square:before { + content: "\f174"; +} +.fa-long-arrow-down:before { + content: "\f175"; +} +.fa-long-arrow-up:before { + content: "\f176"; +} +.fa-long-arrow-left:before { + content: "\f177"; +} +.fa-long-arrow-right:before { + content: "\f178"; +} +.fa-apple:before { + content: "\f179"; +} +.fa-windows:before { + content: "\f17a"; +} +.fa-android:before { + content: "\f17b"; +} +.fa-linux:before { + content: "\f17c"; +} +.fa-dribbble:before { + content: "\f17d"; +} +.fa-skype:before { + content: "\f17e"; +} +.fa-foursquare:before { + content: "\f180"; +} +.fa-trello:before { + content: "\f181"; +} +.fa-female:before { + content: "\f182"; +} +.fa-male:before { + content: "\f183"; +} +.fa-gittip:before, +.fa-gratipay:before { + content: "\f184"; +} +.fa-sun-o:before { + content: "\f185"; +} +.fa-moon-o:before { + content: "\f186"; +} +.fa-archive:before { + content: "\f187"; +} +.fa-bug:before { + content: "\f188"; +} +.fa-vk:before { + content: "\f189"; +} +.fa-weibo:before { + content: "\f18a"; +} +.fa-renren:before { + content: "\f18b"; +} +.fa-pagelines:before { + content: "\f18c"; +} +.fa-stack-exchange:before { + content: "\f18d"; +} +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} +.fa-arrow-circle-o-left:before { + content: "\f190"; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} +.fa-dot-circle-o:before { + content: "\f192"; +} +.fa-wheelchair:before { + content: "\f193"; +} +.fa-vimeo-square:before { + content: "\f194"; +} +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} +.fa-plus-square-o:before { + content: "\f196"; +} +.fa-space-shuttle:before { + content: "\f197"; +} +.fa-slack:before { + content: "\f198"; +} +.fa-envelope-square:before { + content: "\f199"; +} +.fa-wordpress:before { + content: "\f19a"; +} +.fa-openid:before { + content: "\f19b"; +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "\f19c"; +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "\f19d"; +} +.fa-yahoo:before { + content: "\f19e"; +} +.fa-google:before { + content: "\f1a0"; +} +.fa-reddit:before { + content: "\f1a1"; +} +.fa-reddit-square:before { + content: "\f1a2"; +} +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} +.fa-stumbleupon:before { + content: "\f1a4"; +} +.fa-delicious:before { + content: "\f1a5"; +} +.fa-digg:before { + content: "\f1a6"; +} +.fa-pied-piper:before { + content: "\f1a7"; +} +.fa-pied-piper-alt:before { + content: "\f1a8"; +} +.fa-drupal:before { + content: "\f1a9"; +} +.fa-joomla:before { + content: "\f1aa"; +} +.fa-language:before { + content: "\f1ab"; +} +.fa-fax:before { + content: "\f1ac"; +} +.fa-building:before { + content: "\f1ad"; +} +.fa-child:before { + content: "\f1ae"; +} +.fa-paw:before { + content: "\f1b0"; +} +.fa-spoon:before { + content: "\f1b1"; +} +.fa-cube:before { + content: "\f1b2"; +} +.fa-cubes:before { + content: "\f1b3"; +} +.fa-behance:before { + content: "\f1b4"; +} +.fa-behance-square:before { + content: "\f1b5"; +} +.fa-steam:before { + content: "\f1b6"; +} +.fa-steam-square:before { + content: "\f1b7"; +} +.fa-recycle:before { + content: "\f1b8"; +} +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} +.fa-tree:before { + content: "\f1bb"; +} +.fa-spotify:before { + content: "\f1bc"; +} +.fa-deviantart:before { + content: "\f1bd"; +} +.fa-soundcloud:before { + content: "\f1be"; +} +.fa-database:before { + content: "\f1c0"; +} +.fa-file-pdf-o:before { + content: "\f1c1"; +} +.fa-file-word-o:before { + content: "\f1c2"; +} +.fa-file-excel-o:before { + content: "\f1c3"; +} +.fa-file-powerpoint-o:before { + content: "\f1c4"; +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "\f1c5"; +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "\f1c6"; +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "\f1c7"; +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "\f1c8"; +} +.fa-file-code-o:before { + content: "\f1c9"; +} +.fa-vine:before { + content: "\f1ca"; +} +.fa-codepen:before { + content: "\f1cb"; +} +.fa-jsfiddle:before { + content: "\f1cc"; +} +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "\f1cd"; +} +.fa-circle-o-notch:before { + content: "\f1ce"; +} +.fa-ra:before, +.fa-rebel:before { + content: "\f1d0"; +} +.fa-ge:before, +.fa-empire:before { + content: "\f1d1"; +} +.fa-git-square:before { + content: "\f1d2"; +} +.fa-git:before { + content: "\f1d3"; +} +.fa-hacker-news:before { + content: "\f1d4"; +} +.fa-tencent-weibo:before { + content: "\f1d5"; +} +.fa-qq:before { + content: "\f1d6"; +} +.fa-wechat:before, +.fa-weixin:before { + content: "\f1d7"; +} +.fa-send:before, +.fa-paper-plane:before { + content: "\f1d8"; +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "\f1d9"; +} +.fa-history:before { + content: "\f1da"; +} +.fa-genderless:before, +.fa-circle-thin:before { + content: "\f1db"; +} +.fa-header:before { + content: "\f1dc"; +} +.fa-paragraph:before { + content: "\f1dd"; +} +.fa-sliders:before { + content: "\f1de"; +} +.fa-share-alt:before { + content: "\f1e0"; +} +.fa-share-alt-square:before { + content: "\f1e1"; +} +.fa-bomb:before { + content: "\f1e2"; +} +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: "\f1e3"; +} +.fa-tty:before { + content: "\f1e4"; +} +.fa-binoculars:before { + content: "\f1e5"; +} +.fa-plug:before { + content: "\f1e6"; +} +.fa-slideshare:before { + content: "\f1e7"; +} +.fa-twitch:before { + content: "\f1e8"; +} +.fa-yelp:before { + content: "\f1e9"; +} +.fa-newspaper-o:before { + content: "\f1ea"; +} +.fa-wifi:before { + content: "\f1eb"; +} +.fa-calculator:before { + content: "\f1ec"; +} +.fa-paypal:before { + content: "\f1ed"; +} +.fa-google-wallet:before { + content: "\f1ee"; +} +.fa-cc-visa:before { + content: "\f1f0"; +} +.fa-cc-mastercard:before { + content: "\f1f1"; +} +.fa-cc-discover:before { + content: "\f1f2"; +} +.fa-cc-amex:before { + content: "\f1f3"; +} +.fa-cc-paypal:before { + content: "\f1f4"; +} +.fa-cc-stripe:before { + content: "\f1f5"; +} +.fa-bell-slash:before { + content: "\f1f6"; +} +.fa-bell-slash-o:before { + content: "\f1f7"; +} +.fa-trash:before { + content: "\f1f8"; +} +.fa-copyright:before { + content: "\f1f9"; +} +.fa-at:before { + content: "\f1fa"; +} +.fa-eyedropper:before { + content: "\f1fb"; +} +.fa-paint-brush:before { + content: "\f1fc"; +} +.fa-birthday-cake:before { + content: "\f1fd"; +} +.fa-area-chart:before { + content: "\f1fe"; +} +.fa-pie-chart:before { + content: "\f200"; +} +.fa-line-chart:before { + content: "\f201"; +} +.fa-lastfm:before { + content: "\f202"; +} +.fa-lastfm-square:before { + content: "\f203"; +} +.fa-toggle-off:before { + content: "\f204"; +} +.fa-toggle-on:before { + content: "\f205"; +} +.fa-bicycle:before { + content: "\f206"; +} +.fa-bus:before { + content: "\f207"; +} +.fa-ioxhost:before { + content: "\f208"; +} +.fa-angellist:before { + content: "\f209"; +} +.fa-cc:before { + content: "\f20a"; +} +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: "\f20b"; +} +.fa-meanpath:before { + content: "\f20c"; +} +.fa-buysellads:before { + content: "\f20d"; +} +.fa-connectdevelop:before { + content: "\f20e"; +} +.fa-dashcube:before { + content: "\f210"; +} +.fa-forumbee:before { + content: "\f211"; +} +.fa-leanpub:before { + content: "\f212"; +} +.fa-sellsy:before { + content: "\f213"; +} +.fa-shirtsinbulk:before { + content: "\f214"; +} +.fa-simplybuilt:before { + content: "\f215"; +} +.fa-skyatlas:before { + content: "\f216"; +} +.fa-cart-plus:before { + content: "\f217"; +} +.fa-cart-arrow-down:before { + content: "\f218"; +} +.fa-diamond:before { + content: "\f219"; +} +.fa-ship:before { + content: "\f21a"; +} +.fa-user-secret:before { + content: "\f21b"; +} +.fa-motorcycle:before { + content: "\f21c"; +} +.fa-street-view:before { + content: "\f21d"; +} +.fa-heartbeat:before { + content: "\f21e"; +} +.fa-venus:before { + content: "\f221"; +} +.fa-mars:before { + content: "\f222"; +} +.fa-mercury:before { + content: "\f223"; +} +.fa-transgender:before { + content: "\f224"; +} +.fa-transgender-alt:before { + content: "\f225"; +} +.fa-venus-double:before { + content: "\f226"; +} +.fa-mars-double:before { + content: "\f227"; +} +.fa-venus-mars:before { + content: "\f228"; +} +.fa-mars-stroke:before { + content: "\f229"; +} +.fa-mars-stroke-v:before { + content: "\f22a"; +} +.fa-mars-stroke-h:before { + content: "\f22b"; +} +.fa-neuter:before { + content: "\f22c"; +} +.fa-facebook-official:before { + content: "\f230"; +} +.fa-pinterest-p:before { + content: "\f231"; +} +.fa-whatsapp:before { + content: "\f232"; +} +.fa-server:before { + content: "\f233"; +} +.fa-user-plus:before { + content: "\f234"; +} +.fa-user-times:before { + content: "\f235"; +} +.fa-hotel:before, +.fa-bed:before { + content: "\f236"; +} +.fa-viacoin:before { + content: "\f237"; +} +.fa-train:before { + content: "\f238"; +} +.fa-subway:before { + content: "\f239"; +} +.fa-medium:before { + content: "\f23a"; +} diff --git a/frontend/css/vendors/font-awesome.min.css b/frontend/css/vendors/font-awesome.min.css new file mode 100644 index 0000000..24fcc04 --- /dev/null +++ b/frontend/css/vendors/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.3.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.3.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.3.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.3.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.3.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.3.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;transform:translate(0, 0)}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-genderless:before,.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"} \ No newline at end of file diff --git a/frontend/css/vendors/jquery-ui-1.10.3.custom.css b/frontend/css/vendors/jquery-ui-1.10.3.custom.css new file mode 100644 index 0000000..95aec2f --- /dev/null +++ b/frontend/css/vendors/jquery-ui-1.10.3.custom.css @@ -0,0 +1,1177 @@ +/*! jQuery UI - v1.10.3 - 2013-09-28 +* http://jqueryui.com +* Includes: jquery.ui.core.css, jquery.ui.resizable.css, jquery.ui.selectable.css, jquery.ui.accordion.css, jquery.ui.autocomplete.css, jquery.ui.button.css, jquery.ui.datepicker.css, jquery.ui.dialog.css, jquery.ui.menu.css, jquery.ui.progressbar.css, jquery.ui.slider.css, jquery.ui.spinner.css, jquery.ui.tabs.css, jquery.ui.tooltip.css, jquery.ui.theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px +* Copyright 2013 jQuery Foundation and other contributors; Licensed MIT */ + +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { + display: none; +} +.ui-helper-hidden-accessible { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} +.ui-helper-reset { + margin: 0; + padding: 0; + border: 0; + outline: 0; + line-height: 1.3; + text-decoration: none; + font-size: 100%; + list-style: none; +} +.ui-helper-clearfix:before, +.ui-helper-clearfix:after { + content: ""; + display: table; + border-collapse: collapse; +} +.ui-helper-clearfix:after { + clear: both; +} +.ui-helper-clearfix { + min-height: 0; /* support: IE7 */ +} +.ui-helper-zfix { + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + opacity: 0; + filter:Alpha(Opacity=0); +} + +.ui-front { + z-index: 100; +} + + +/* Interaction Cues +----------------------------------*/ +.ui-state-disabled { + cursor: default !important; +} + + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + display: block; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; +} + + +/* Misc visuals +----------------------------------*/ + +/* Overlays */ +.ui-widget-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.ui-resizable { + position: relative; +} +.ui-resizable-handle { + position: absolute; + font-size: 0.1px; + display: block; +} +.ui-resizable-disabled .ui-resizable-handle, +.ui-resizable-autohide .ui-resizable-handle { + display: none; +} +.ui-resizable-n { + cursor: n-resize; + height: 7px; + width: 100%; + top: -5px; + left: 0; +} +.ui-resizable-s { + cursor: s-resize; + height: 7px; + width: 100%; + bottom: -5px; + left: 0; +} +.ui-resizable-e { + cursor: e-resize; + width: 7px; + right: -5px; + top: 0; + height: 100%; +} +.ui-resizable-w { + cursor: w-resize; + width: 7px; + left: -5px; + top: 0; + height: 100%; +} +.ui-resizable-se { + cursor: se-resize; + width: 12px; + height: 12px; + right: 1px; + bottom: 1px; +} +.ui-resizable-sw { + cursor: sw-resize; + width: 9px; + height: 9px; + left: -5px; + bottom: -5px; +} +.ui-resizable-nw { + cursor: nw-resize; + width: 9px; + height: 9px; + left: -5px; + top: -5px; +} +.ui-resizable-ne { + cursor: ne-resize; + width: 9px; + height: 9px; + right: -5px; + top: -5px; +} +.ui-selectable-helper { + position: absolute; + z-index: 100; + border: 1px dotted black; +} +.ui-accordion .ui-accordion-header { + display: block; + cursor: pointer; + position: relative; + margin-top: 2px; + padding: .5em .5em .5em .7em; + min-height: 0; /* support: IE7 */ +} +.ui-accordion .ui-accordion-icons { + padding-left: 2.2em; +} +.ui-accordion .ui-accordion-noicons { + padding-left: .7em; +} +.ui-accordion .ui-accordion-icons .ui-accordion-icons { + padding-left: 2.2em; +} +.ui-accordion .ui-accordion-header .ui-accordion-header-icon { + position: absolute; + left: .5em; + top: 50%; + margin-top: -8px; +} +.ui-accordion .ui-accordion-content { + padding: 1em 2.2em; + border-top: 0; + overflow: auto; +} +.ui-autocomplete { + position: absolute; + top: 0; + left: 0; + cursor: default; +} +.ui-button { + display: inline-block; + position: relative; + padding: 0; + line-height: normal; + margin-right: .1em; + cursor: pointer; + vertical-align: middle; + text-align: center; + overflow: visible; /* removes extra width in IE */ +} +.ui-button, +.ui-button:link, +.ui-button:visited, +.ui-button:hover, +.ui-button:active { + text-decoration: none; +} +/* to make room for the icon, a width needs to be set here */ +.ui-button-icon-only { + width: 2.2em; +} +/* button elements seem to need a little more width */ +button.ui-button-icon-only { + width: 2.4em; +} +.ui-button-icons-only { + width: 3.4em; +} +button.ui-button-icons-only { + width: 3.7em; +} + +/* button text element */ +.ui-button .ui-button-text { + display: block; + line-height: normal; +} +.ui-button-text-only .ui-button-text { + padding: .4em 1em; +} +.ui-button-icon-only .ui-button-text, +.ui-button-icons-only .ui-button-text { + padding: .4em; + text-indent: -9999999px; +} +.ui-button-text-icon-primary .ui-button-text, +.ui-button-text-icons .ui-button-text { + padding: .4em 1em .4em 2.1em; +} +.ui-button-text-icon-secondary .ui-button-text, +.ui-button-text-icons .ui-button-text { + padding: .4em 2.1em .4em 1em; +} +.ui-button-text-icons .ui-button-text { + padding-left: 2.1em; + padding-right: 2.1em; +} +/* no icon support for input elements, provide padding by default */ +input.ui-button { + padding: .4em 1em; +} + +/* button icon element(s) */ +.ui-button-icon-only .ui-icon, +.ui-button-text-icon-primary .ui-icon, +.ui-button-text-icon-secondary .ui-icon, +.ui-button-text-icons .ui-icon, +.ui-button-icons-only .ui-icon { + position: absolute; + top: 50%; + margin-top: -8px; +} +.ui-button-icon-only .ui-icon { + left: 50%; + margin-left: -8px; +} +.ui-button-text-icon-primary .ui-button-icon-primary, +.ui-button-text-icons .ui-button-icon-primary, +.ui-button-icons-only .ui-button-icon-primary { + left: .5em; +} +.ui-button-text-icon-secondary .ui-button-icon-secondary, +.ui-button-text-icons .ui-button-icon-secondary, +.ui-button-icons-only .ui-button-icon-secondary { + right: .5em; +} + +/* button sets */ +.ui-buttonset { + margin-right: 7px; +} +.ui-buttonset .ui-button { + margin-left: 0; + margin-right: -.3em; +} + +/* workarounds */ +/* reset extra padding in Firefox, see h5bp.com/l */ +input.ui-button::-moz-focus-inner, +button.ui-button::-moz-focus-inner { + border: 0; + padding: 0; +} +.ui-datepicker { + width: 17em; + padding: .2em .2em 0; + display: none; +} +.ui-datepicker .ui-datepicker-header { + position: relative; + padding: .2em 0; +} +.ui-datepicker .ui-datepicker-prev, +.ui-datepicker .ui-datepicker-next { + position: absolute; + top: 2px; + width: 1.8em; + height: 1.8em; +} +.ui-datepicker .ui-datepicker-prev-hover, +.ui-datepicker .ui-datepicker-next-hover { + top: 1px; +} +.ui-datepicker .ui-datepicker-prev { + left: 2px; +} +.ui-datepicker .ui-datepicker-next { + right: 2px; +} +.ui-datepicker .ui-datepicker-prev-hover { + left: 1px; +} +.ui-datepicker .ui-datepicker-next-hover { + right: 1px; +} +.ui-datepicker .ui-datepicker-prev span, +.ui-datepicker .ui-datepicker-next span { + display: block; + position: absolute; + left: 50%; + margin-left: -8px; + top: 50%; + margin-top: -8px; +} +.ui-datepicker .ui-datepicker-title { + margin: 0 2.3em; + line-height: 1.8em; + text-align: center; +} +.ui-datepicker .ui-datepicker-title select { + font-size: 1em; + margin: 1px 0; +} +.ui-datepicker select.ui-datepicker-month-year { + width: 100%; +} +.ui-datepicker select.ui-datepicker-month, +.ui-datepicker select.ui-datepicker-year { + width: 49%; +} +.ui-datepicker table { + width: 100%; + font-size: .9em; + border-collapse: collapse; + margin: 0 0 .4em; +} +.ui-datepicker th { + padding: .7em .3em; + text-align: center; + font-weight: bold; + border: 0; +} +.ui-datepicker td { + border: 0; + padding: 1px; +} +.ui-datepicker td span, +.ui-datepicker td a { + display: block; + padding: .2em; + text-align: right; + text-decoration: none; +} +.ui-datepicker .ui-datepicker-buttonpane { + background-image: none; + margin: .7em 0 0 0; + padding: 0 .2em; + border-left: 0; + border-right: 0; + border-bottom: 0; +} +.ui-datepicker .ui-datepicker-buttonpane button { + float: right; + margin: .5em .2em .4em; + cursor: pointer; + padding: .2em .6em .3em .6em; + width: auto; + overflow: visible; +} +.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { + float: left; +} + +/* with multiple calendars */ +.ui-datepicker.ui-datepicker-multi { + width: auto; +} +.ui-datepicker-multi .ui-datepicker-group { + float: left; +} +.ui-datepicker-multi .ui-datepicker-group table { + width: 95%; + margin: 0 auto .4em; +} +.ui-datepicker-multi-2 .ui-datepicker-group { + width: 50%; +} +.ui-datepicker-multi-3 .ui-datepicker-group { + width: 33.3%; +} +.ui-datepicker-multi-4 .ui-datepicker-group { + width: 25%; +} +.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { + border-left-width: 0; +} +.ui-datepicker-multi .ui-datepicker-buttonpane { + clear: left; +} +.ui-datepicker-row-break { + clear: both; + width: 100%; + font-size: 0; +} + +/* RTL support */ +.ui-datepicker-rtl { + direction: rtl; +} +.ui-datepicker-rtl .ui-datepicker-prev { + right: 2px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next { + left: 2px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-prev:hover { + right: 1px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next:hover { + left: 1px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane { + clear: right; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button { + float: left; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current, +.ui-datepicker-rtl .ui-datepicker-group { + float: right; +} +.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { + border-right-width: 0; + border-left-width: 1px; +} +.ui-dialog { + position: absolute; + top: 0; + left: 0; + padding: .2em; + outline: 0; +} +.ui-dialog .ui-dialog-titlebar { + padding: .4em 1em; + position: relative; +} +.ui-dialog .ui-dialog-title { + float: left; + margin: .1em 0; + white-space: nowrap; + width: 90%; + overflow: hidden; + text-overflow: ellipsis; +} +.ui-dialog .ui-dialog-titlebar-close { + position: absolute; + right: .3em; + top: 50%; + width: 21px; + margin: -10px 0 0 0; + padding: 1px; + height: 20px; +} +.ui-dialog .ui-dialog-content { + position: relative; + border: 0; + padding: .5em 1em; + background: none; + overflow: auto; +} +.ui-dialog .ui-dialog-buttonpane { + text-align: left; + border-width: 1px 0 0 0; + background-image: none; + margin-top: .5em; + padding: .3em 1em .5em .4em; +} +.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { + float: right; +} +.ui-dialog .ui-dialog-buttonpane button { + margin: .5em .4em .5em 0; + cursor: pointer; +} +.ui-dialog .ui-resizable-se { + width: 12px; + height: 12px; + right: -5px; + bottom: -5px; + background-position: 16px 16px; +} +.ui-draggable .ui-dialog-titlebar { + cursor: move; +} +.ui-menu { + list-style: none; + padding: 2px; + margin: 0; + display: block; + outline: none; +} +.ui-menu .ui-menu { + margin-top: -3px; + position: absolute; +} +.ui-menu .ui-menu-item { + margin: 0; + padding: 0; + width: 100%; + /* support: IE10, see #8844 */ + list-style-image: url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7); +} +.ui-menu .ui-menu-divider { + margin: 5px -2px 5px -2px; + height: 0; + font-size: 0; + line-height: 0; + border-width: 1px 0 0 0; +} +.ui-menu .ui-menu-item a { + text-decoration: none; + display: block; + padding: 2px .4em; + line-height: 1.5; + min-height: 0; /* support: IE7 */ + font-weight: normal; +} +.ui-menu .ui-menu-item a.ui-state-focus, +.ui-menu .ui-menu-item a.ui-state-active { + font-weight: normal; + margin: -1px; +} + +.ui-menu .ui-state-disabled { + font-weight: normal; + margin: .4em 0 .2em; + line-height: 1.5; +} +.ui-menu .ui-state-disabled a { + cursor: default; +} + +/* icon support */ +.ui-menu-icons { + position: relative; +} +.ui-menu-icons .ui-menu-item a { + position: relative; + padding-left: 2em; +} + +/* left-aligned */ +.ui-menu .ui-icon { + position: absolute; + top: .2em; + left: .2em; +} + +/* right-aligned */ +.ui-menu .ui-menu-icon { + position: static; + float: right; +} +.ui-progressbar { + height: 2em; + text-align: left; + overflow: hidden; +} +.ui-progressbar .ui-progressbar-value { + margin: -1px; + height: 100%; +} +.ui-progressbar .ui-progressbar-overlay { + background: url("images/animated-overlay.gif"); + height: 100%; + filter: alpha(opacity=25); + opacity: 0.25; +} +.ui-progressbar-indeterminate .ui-progressbar-value { + background-image: none; +} +.ui-slider { + position: relative; + text-align: left; +} +.ui-slider .ui-slider-handle { + position: absolute; + z-index: 2; + width: 1.2em; + height: 1.2em; + cursor: default; +} +.ui-slider .ui-slider-range { + position: absolute; + z-index: 1; + font-size: .7em; + display: block; + border: 0; + background-position: 0 0; +} + +/* For IE8 - See #6727 */ +.ui-slider.ui-state-disabled .ui-slider-handle, +.ui-slider.ui-state-disabled .ui-slider-range { + filter: inherit; +} + +.ui-slider-horizontal { + height: .8em; +} +.ui-slider-horizontal .ui-slider-handle { + top: -.3em; + margin-left: -.6em; +} +.ui-slider-horizontal .ui-slider-range { + top: 0; + height: 100%; +} +.ui-slider-horizontal .ui-slider-range-min { + left: 0; +} +.ui-slider-horizontal .ui-slider-range-max { + right: 0; +} + +.ui-slider-vertical { + width: .8em; + height: 100px; +} +.ui-slider-vertical .ui-slider-handle { + left: -.3em; + margin-left: 0; + margin-bottom: -.6em; +} +.ui-slider-vertical .ui-slider-range { + left: 0; + width: 100%; +} +.ui-slider-vertical .ui-slider-range-min { + bottom: 0; +} +.ui-slider-vertical .ui-slider-range-max { + top: 0; +} +.ui-spinner { + position: relative; + display: inline-block; + overflow: hidden; + padding: 0; + vertical-align: middle; +} +.ui-spinner-input { + border: none; + background: none; + color: inherit; + padding: 0; + margin: .2em 0; + vertical-align: middle; + margin-left: .4em; + margin-right: 22px; +} +.ui-spinner-button { + width: 16px; + height: 50%; + font-size: .5em; + padding: 0; + margin: 0; + text-align: center; + position: absolute; + cursor: default; + display: block; + overflow: hidden; + right: 0; +} +/* more specificity required here to overide default borders */ +.ui-spinner a.ui-spinner-button { + border-top: none; + border-bottom: none; + border-right: none; +} +/* vertical centre icon */ +.ui-spinner .ui-icon { + position: absolute; + margin-top: -8px; + top: 50%; + left: 0; +} +.ui-spinner-up { + top: 0; +} +.ui-spinner-down { + bottom: 0; +} + +/* TR overrides */ +.ui-spinner .ui-icon-triangle-1-s { + /* need to fix icons sprite */ + background-position: -65px -16px; +} +.ui-tabs { + position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ + padding: .2em; +} +.ui-tabs .ui-tabs-nav { + margin: 0; + padding: .2em .2em 0; +} +.ui-tabs .ui-tabs-nav li { + list-style: none; + float: left; + position: relative; + top: 0; + margin: 1px .2em 0 0; + border-bottom-width: 0; + padding: 0; + white-space: nowrap; +} +.ui-tabs .ui-tabs-nav li a { + float: left; + padding: .5em 1em; + text-decoration: none; +} +.ui-tabs .ui-tabs-nav li.ui-tabs-active { + margin-bottom: -1px; + padding-bottom: 1px; +} +.ui-tabs .ui-tabs-nav li.ui-tabs-active a, +.ui-tabs .ui-tabs-nav li.ui-state-disabled a, +.ui-tabs .ui-tabs-nav li.ui-tabs-loading a { + cursor: text; +} +.ui-tabs .ui-tabs-nav li a, /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ +.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active a { + cursor: pointer; +} +.ui-tabs .ui-tabs-panel { + display: block; + border-width: 0; + padding: 1em 1.4em; + background: none; +} +.ui-tooltip { + padding: 8px; + position: absolute; + z-index: 9999; + max-width: 300px; + -webkit-box-shadow: 0 0 5px #aaa; + box-shadow: 0 0 5px #aaa; +} +body .ui-tooltip { + border-width: 2px; +} + +/* Component containers +----------------------------------*/ +.ui-widget { + font-family: Verdana,Arial,sans-serif; + font-size: 1.1em; +} +.ui-widget .ui-widget { + font-size: 1em; +} +.ui-widget input, +.ui-widget select, +.ui-widget textarea, +.ui-widget button { + font-family: Verdana,Arial,sans-serif; + font-size: 1em; +} +.ui-widget-content { + border: 1px solid #aaaaaa; + background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x; + color: #222222; +} +.ui-widget-content a { + color: #222222; +} +.ui-widget-header { + border: 1px solid #aaaaaa; + background: #cccccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x; + color: #222222; + font-weight: bold; +} +.ui-widget-header a { + color: #222222; +} + +/* Interaction states +----------------------------------*/ +.ui-state-default, +.ui-widget-content .ui-state-default, +.ui-widget-header .ui-state-default { + border: 1px solid #d3d3d3; + background: #e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x; + font-weight: normal; + color: #555555; +} +.ui-state-default a, +.ui-state-default a:link, +.ui-state-default a:visited { + color: #555555; + text-decoration: none; +} +.ui-state-hover, +.ui-widget-content .ui-state-hover, +.ui-widget-header .ui-state-hover, +.ui-state-focus, +.ui-widget-content .ui-state-focus, +.ui-widget-header .ui-state-focus { + border: 1px solid #999999; + background: #dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x; + font-weight: normal; + color: #212121; +} +.ui-state-hover a, +.ui-state-hover a:hover, +.ui-state-hover a:link, +.ui-state-hover a:visited { + color: #212121; + text-decoration: none; +} +.ui-state-active, +.ui-widget-content .ui-state-active, +.ui-widget-header .ui-state-active { + border: 1px solid #aaaaaa; + background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x; + font-weight: normal; + color: #212121; +} +.ui-state-active a, +.ui-state-active a:link, +.ui-state-active a:visited { + color: #212121; + text-decoration: none; +} + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, +.ui-widget-content .ui-state-highlight, +.ui-widget-header .ui-state-highlight { + border: 1px solid #fcefa1; + background: #fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x; + color: #363636; +} +.ui-state-highlight a, +.ui-widget-content .ui-state-highlight a, +.ui-widget-header .ui-state-highlight a { + color: #363636; +} +.ui-state-error, +.ui-widget-content .ui-state-error, +.ui-widget-header .ui-state-error { + border: 1px solid #cd0a0a; + background: #fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x; + color: #cd0a0a; +} +.ui-state-error a, +.ui-widget-content .ui-state-error a, +.ui-widget-header .ui-state-error a { + color: #cd0a0a; +} +.ui-state-error-text, +.ui-widget-content .ui-state-error-text, +.ui-widget-header .ui-state-error-text { + color: #cd0a0a; +} +.ui-priority-primary, +.ui-widget-content .ui-priority-primary, +.ui-widget-header .ui-priority-primary { + font-weight: bold; +} +.ui-priority-secondary, +.ui-widget-content .ui-priority-secondary, +.ui-widget-header .ui-priority-secondary { + opacity: .7; + filter:Alpha(Opacity=70); + font-weight: normal; +} +.ui-state-disabled, +.ui-widget-content .ui-state-disabled, +.ui-widget-header .ui-state-disabled { + opacity: .35; + filter:Alpha(Opacity=35); + background-image: none; +} +.ui-state-disabled .ui-icon { + filter:Alpha(Opacity=35); /* For IE8 - See #6059 */ +} + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + width: 16px; + height: 16px; +} +.ui-icon, +.ui-widget-content .ui-icon { + background-image: url(images/ui-icons_222222_256x240.png); +} +.ui-widget-header .ui-icon { + background-image: url(images/ui-icons_222222_256x240.png); +} +.ui-state-default .ui-icon { + background-image: url(images/ui-icons_888888_256x240.png); +} +.ui-state-hover .ui-icon, +.ui-state-focus .ui-icon { + background-image: url(images/ui-icons_454545_256x240.png); +} +.ui-state-active .ui-icon { + background-image: url(images/ui-icons_454545_256x240.png); +} +.ui-state-highlight .ui-icon { + background-image: url(images/ui-icons_2e83ff_256x240.png); +} +.ui-state-error .ui-icon, +.ui-state-error-text .ui-icon { + background-image: url(images/ui-icons_cd0a0a_256x240.png); +} + +/* positioning */ +.ui-icon-blank { background-position: 16px 16px; } +.ui-icon-carat-1-n { background-position: 0 0; } +.ui-icon-carat-1-ne { background-position: -16px 0; } +.ui-icon-carat-1-e { background-position: -32px 0; } +.ui-icon-carat-1-se { background-position: -48px 0; } +.ui-icon-carat-1-s { background-position: -64px 0; } +.ui-icon-carat-1-sw { background-position: -80px 0; } +.ui-icon-carat-1-w { background-position: -96px 0; } +.ui-icon-carat-1-nw { background-position: -112px 0; } +.ui-icon-carat-2-n-s { background-position: -128px 0; } +.ui-icon-carat-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -64px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -64px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 0 -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-on { background-position: -96px -144px; } +.ui-icon-radio-off { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-start { background-position: -80px -160px; } +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, +.ui-corner-top, +.ui-corner-left, +.ui-corner-tl { + border-top-left-radius: 4px; +} +.ui-corner-all, +.ui-corner-top, +.ui-corner-right, +.ui-corner-tr { + border-top-right-radius: 4px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-left, +.ui-corner-bl { + border-bottom-left-radius: 4px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-right, +.ui-corner-br { + border-bottom-right-radius: 4px; +} + +/* Overlays */ +.ui-widget-overlay { + background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; + opacity: .3; + filter: Alpha(Opacity=30); +} +.ui-widget-shadow { + margin: -8px 0 0 -8px; + padding: 8px; + background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; + opacity: .3; + filter: Alpha(Opacity=30); + border-radius: 8px; +} diff --git a/frontend/css/vendors/jquery-ui-1.10.3.custom.min.css b/frontend/css/vendors/jquery-ui-1.10.3.custom.min.css new file mode 100644 index 0000000..33cb19b --- /dev/null +++ b/frontend/css/vendors/jquery-ui-1.10.3.custom.min.css @@ -0,0 +1,7 @@ +/*! jQuery UI - v1.10.3 - 2013-09-28 +* http://jqueryui.com +* Includes: jquery.ui.core.css, jquery.ui.resizable.css, jquery.ui.selectable.css, jquery.ui.accordion.css, jquery.ui.autocomplete.css, jquery.ui.button.css, jquery.ui.datepicker.css, jquery.ui.dialog.css, jquery.ui.menu.css, jquery.ui.progressbar.css, jquery.ui.slider.css, jquery.ui.spinner.css, jquery.ui.tabs.css, jquery.ui.tooltip.css, jquery.ui.theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px +* Copyright 2013 jQuery Foundation and other contributors; Licensed MIT */ + +.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin-top:2px;padding:.5em .5em .5em .7em;min-height:0}.ui-accordion .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-noicons{padding-left:.7em}.ui-accordion .ui-accordion-icons .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-header .ui-accordion-header-icon{position:absolute;left:.5em;top:50%;margin-top:-8px}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-button{display:inline-block;position:relative;padding:0;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:normal}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon,.ui-button-icons-only .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary,.ui-button-icons-only .ui-button-icon-primary{left:.5em}.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-button{margin-left:0;margin-right:-.3em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month-year{width:100%}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:49%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-dialog{position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:21px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:12px;height:12px;right:-5px;bottom:-5px;background-position:16px 16px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-menu{list-style:none;padding:2px;margin:0;display:block;outline:none}.ui-menu .ui-menu{margin-top:-3px;position:absolute}.ui-menu .ui-menu-item{margin:0;padding:0;width:100%;list-style-image:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}.ui-menu .ui-menu-divider{margin:5px -2px 5px -2px;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-menu-item a{text-decoration:none;display:block;padding:2px .4em;line-height:1.5;min-height:0;font-weight:normal}.ui-menu .ui-menu-item a.ui-state-focus,.ui-menu .ui-menu-item a.ui-state-active{font-weight:normal;margin:-1px}.ui-menu .ui-state-disabled{font-weight:normal;margin:.4em 0 .2em;line-height:1.5}.ui-menu .ui-state-disabled a{cursor:default}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item a{position:relative;padding-left:2em}.ui-menu .ui-icon{position:absolute;top:.2em;left:.2em}.ui-menu .ui-menu-icon{position:static;float:right}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("images/animated-overlay.gif");height:100%;filter:alpha(opacity=25);opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:22px}.ui-spinner-button{width:16px;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top:none;border-bottom:none;border-right:none}.ui-spinner .ui-icon{position:absolute;margin-top:-8px;top:50%;left:0}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-spinner .ui-icon-triangle-1-s{background-position:-65px -16px}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav li a{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active a,.ui-tabs .ui-tabs-nav li.ui-state-disabled a,.ui-tabs .ui-tabs-nav li.ui-tabs-loading a{cursor:text}.ui-tabs .ui-tabs-nav li a,.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active a{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px;-webkit-box-shadow:0 0 5px #aaa;box-shadow:0 0 5px #aaa}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #aaa;background:#fff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x;color:#222}.ui-widget-content a{color:#222}.ui-widget-header{border:1px solid #aaa;background:#ccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x;color:#222;font-weight:bold}.ui-widget-header a{color:#222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #d3d3d3;background:#e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#555}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#555;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #999;background:#dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited{color:#212121;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #aaa;background:#fff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-widget-header .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-state-default .ui-icon{background-image:url(images/ui-icons_888888_256x240.png)}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-active .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-highlight .ui-icon{background-image:url(images/ui-icons_2e83ff_256x240.png)}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url(images/ui-icons_cd0a0a_256x240.png)}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px 0 0 -8px;padding:8px;background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30);border-radius:8px} \ No newline at end of file diff --git a/frontend/css/vendors/less/.csscomb.json b/frontend/css/vendors/less/.csscomb.json new file mode 100644 index 0000000..40695a4 --- /dev/null +++ b/frontend/css/vendors/less/.csscomb.json @@ -0,0 +1,304 @@ +{ + "always-semicolon": true, + "block-indent": 2, + "color-case": "lower", + "color-shorthand": true, + "element-case": "lower", + "eof-newline": true, + "leading-zero": false, + "remove-empty-rulesets": true, + "space-after-colon": 1, + "space-after-combinator": 1, + "space-before-selector-delimiter": 0, + "space-between-declarations": "\n", + "space-after-opening-brace": "\n", + "space-before-closing-brace": "\n", + "space-before-colon": 0, + "space-before-combinator": 1, + "space-before-opening-brace": 1, + "strip-spaces": true, + "unitless-zero": true, + "vendor-prefix-align": true, + "sort-order": [ + [ + "position", + "top", + "right", + "bottom", + "left", + "z-index", + "display", + "float", + "width", + "min-width", + "max-width", + "height", + "min-height", + "max-height", + "-webkit-box-sizing", + "-moz-box-sizing", + "box-sizing", + "-webkit-appearance", + "padding", + "padding-top", + "padding-right", + "padding-bottom", + "padding-left", + "margin", + "margin-top", + "margin-right", + "margin-bottom", + "margin-left", + "overflow", + "overflow-x", + "overflow-y", + "-webkit-overflow-scrolling", + "-ms-overflow-x", + "-ms-overflow-y", + "-ms-overflow-style", + "clip", + "clear", + "font", + "font-family", + "font-size", + "font-style", + "font-weight", + "font-variant", + "font-size-adjust", + "font-stretch", + "font-effect", + "font-emphasize", + "font-emphasize-position", + "font-emphasize-style", + "font-smooth", + "-webkit-hyphens", + "-moz-hyphens", + "hyphens", + "line-height", + "color", + "text-align", + "-webkit-text-align-last", + "-moz-text-align-last", + "-ms-text-align-last", + "text-align-last", + "text-emphasis", + "text-emphasis-color", + "text-emphasis-style", + "text-emphasis-position", + "text-decoration", + "text-indent", + "text-justify", + "text-outline", + "-ms-text-overflow", + "text-overflow", + "text-overflow-ellipsis", + "text-overflow-mode", + "text-shadow", + "text-transform", + "text-wrap", + "-webkit-text-size-adjust", + "-ms-text-size-adjust", + "letter-spacing", + "-ms-word-break", + "word-break", + "word-spacing", + "-ms-word-wrap", + "word-wrap", + "-moz-tab-size", + "-o-tab-size", + "tab-size", + "white-space", + "vertical-align", + "list-style", + "list-style-position", + "list-style-type", + "list-style-image", + "pointer-events", + "-ms-touch-action", + "touch-action", + "cursor", + "visibility", + "zoom", + "flex-direction", + "flex-order", + "flex-pack", + "flex-align", + "table-layout", + "empty-cells", + "caption-side", + "border-spacing", + "border-collapse", + "content", + "quotes", + "counter-reset", + "counter-increment", + "resize", + "-webkit-user-select", + "-moz-user-select", + "-ms-user-select", + "-o-user-select", + "user-select", + "nav-index", + "nav-up", + "nav-right", + "nav-down", + "nav-left", + "background", + "background-color", + "background-image", + "-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient", + "filter:progid:DXImageTransform.Microsoft.gradient", + "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader", + "filter", + "background-repeat", + "background-attachment", + "background-position", + "background-position-x", + "background-position-y", + "-webkit-background-clip", + "-moz-background-clip", + "background-clip", + "background-origin", + "-webkit-background-size", + "-moz-background-size", + "-o-background-size", + "background-size", + "border", + "border-color", + "border-style", + "border-width", + "border-top", + "border-top-color", + "border-top-style", + "border-top-width", + "border-right", + "border-right-color", + "border-right-style", + "border-right-width", + "border-bottom", + "border-bottom-color", + "border-bottom-style", + "border-bottom-width", + "border-left", + "border-left-color", + "border-left-style", + "border-left-width", + "border-radius", + "border-top-left-radius", + "border-top-right-radius", + "border-bottom-right-radius", + "border-bottom-left-radius", + "-webkit-border-image", + "-moz-border-image", + "-o-border-image", + "border-image", + "-webkit-border-image-source", + "-moz-border-image-source", + "-o-border-image-source", + "border-image-source", + "-webkit-border-image-slice", + "-moz-border-image-slice", + "-o-border-image-slice", + "border-image-slice", + "-webkit-border-image-width", + "-moz-border-image-width", + "-o-border-image-width", + "border-image-width", + "-webkit-border-image-outset", + "-moz-border-image-outset", + "-o-border-image-outset", + "border-image-outset", + "-webkit-border-image-repeat", + "-moz-border-image-repeat", + "-o-border-image-repeat", + "border-image-repeat", + "outline", + "outline-width", + "outline-style", + "outline-color", + "outline-offset", + "-webkit-box-shadow", + "-moz-box-shadow", + "box-shadow", + "filter:progid:DXImageTransform.Microsoft.Alpha(Opacity", + "-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha", + "opacity", + "-ms-interpolation-mode", + "-webkit-transition", + "-moz-transition", + "-ms-transition", + "-o-transition", + "transition", + "-webkit-transition-delay", + "-moz-transition-delay", + "-ms-transition-delay", + "-o-transition-delay", + "transition-delay", + "-webkit-transition-timing-function", + "-moz-transition-timing-function", + "-ms-transition-timing-function", + "-o-transition-timing-function", + "transition-timing-function", + "-webkit-transition-duration", + "-moz-transition-duration", + "-ms-transition-duration", + "-o-transition-duration", + "transition-duration", + "-webkit-transition-property", + "-moz-transition-property", + "-ms-transition-property", + "-o-transition-property", + "transition-property", + "-webkit-transform", + "-moz-transform", + "-ms-transform", + "-o-transform", + "transform", + "-webkit-transform-origin", + "-moz-transform-origin", + "-ms-transform-origin", + "-o-transform-origin", + "transform-origin", + "-webkit-animation", + "-moz-animation", + "-ms-animation", + "-o-animation", + "animation", + "-webkit-animation-name", + "-moz-animation-name", + "-ms-animation-name", + "-o-animation-name", + "animation-name", + "-webkit-animation-duration", + "-moz-animation-duration", + "-ms-animation-duration", + "-o-animation-duration", + "animation-duration", + "-webkit-animation-play-state", + "-moz-animation-play-state", + "-ms-animation-play-state", + "-o-animation-play-state", + "animation-play-state", + "-webkit-animation-timing-function", + "-moz-animation-timing-function", + "-ms-animation-timing-function", + "-o-animation-timing-function", + "animation-timing-function", + "-webkit-animation-delay", + "-moz-animation-delay", + "-ms-animation-delay", + "-o-animation-delay", + "animation-delay", + "-webkit-animation-iteration-count", + "-moz-animation-iteration-count", + "-ms-animation-iteration-count", + "-o-animation-iteration-count", + "animation-iteration-count", + "-webkit-animation-direction", + "-moz-animation-direction", + "-ms-animation-direction", + "-o-animation-direction", + "animation-direction" + ] + ] +} diff --git a/frontend/css/vendors/less/.csslintrc b/frontend/css/vendors/less/.csslintrc new file mode 100644 index 0000000..005b862 --- /dev/null +++ b/frontend/css/vendors/less/.csslintrc @@ -0,0 +1,19 @@ +{ + "adjoining-classes": false, + "box-sizing": false, + "box-model": false, + "compatible-vendor-prefixes": false, + "floats": false, + "font-sizes": false, + "gradients": false, + "important": false, + "known-properties": false, + "outline-none": false, + "qualified-headings": false, + "regex-selectors": false, + "shorthand": false, + "text-indent": false, + "unique-headings": false, + "universal-selector": false, + "unqualified-attributes": false +} diff --git a/frontend/css/vendors/less/alerts.less b/frontend/css/vendors/less/alerts.less new file mode 100644 index 0000000..c4199db --- /dev/null +++ b/frontend/css/vendors/less/alerts.less @@ -0,0 +1,73 @@ +// +// Alerts +// -------------------------------------------------- + + +// Base styles +// ------------------------- + +.alert { + padding: @alert-padding; + margin-bottom: @line-height-computed; + border: 1px solid transparent; + border-radius: @alert-border-radius; + + // Headings for larger alerts + h4 { + margin-top: 0; + // Specified for the h4 to prevent conflicts of changing @headings-color + color: inherit; + } + + // Provide class for links that match alerts + .alert-link { + font-weight: @alert-link-font-weight; + } + + // Improve alignment and spacing of inner content + > p, + > ul { + margin-bottom: 0; + } + + > p + p { + margin-top: 5px; + } +} + +// Dismissible alerts +// +// Expand the right padding and account for the close button's positioning. + +.alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0. +.alert-dismissible { + padding-right: (@alert-padding + 20); + + // Adjust close link position + .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; + } +} + +// Alternate styles +// +// Generate contextual modifier classes for colorizing the alert. + +.alert-success { + .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text); +} + +.alert-info { + .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text); +} + +.alert-warning { + .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text); +} + +.alert-danger { + .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text); +} diff --git a/frontend/css/vendors/less/badges.less b/frontend/css/vendors/less/badges.less new file mode 100644 index 0000000..6ee16dc --- /dev/null +++ b/frontend/css/vendors/less/badges.less @@ -0,0 +1,66 @@ +// +// Badges +// -------------------------------------------------- + + +// Base class +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: @font-size-small; + font-weight: @badge-font-weight; + color: @badge-color; + line-height: @badge-line-height; + vertical-align: middle; + white-space: nowrap; + text-align: center; + background-color: @badge-bg; + border-radius: @badge-border-radius; + + // Empty badges collapse automatically (not available in IE8) + &:empty { + display: none; + } + + // Quick fix for badges in buttons + .btn & { + position: relative; + top: -1px; + } + + .btn-xs &, + .btn-group-xs > .btn & { + top: 0; + padding: 1px 5px; + } + + // Hover state, but only for links + a& { + &:hover, + &:focus { + color: @badge-link-hover-color; + text-decoration: none; + cursor: pointer; + } + } + + // Account for badges in navs + .list-group-item.active > &, + .nav-pills > .active > a > & { + color: @badge-active-color; + background-color: @badge-active-bg; + } + + .list-group-item > & { + float: right; + } + + .list-group-item > & + & { + margin-right: 5px; + } + + .nav-pills > li > a > & { + margin-left: 3px; + } +} diff --git a/frontend/css/vendors/less/bootstrap.less b/frontend/css/vendors/less/bootstrap.less new file mode 100644 index 0000000..feb65fa --- /dev/null +++ b/frontend/css/vendors/less/bootstrap.less @@ -0,0 +1,57 @@ +/*! + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +// Core variables and mixins +@import "variables.less"; +@import "../../custom_bootstrap_variables.less"; +@import "mixins.less"; + +// Reset and dependencies +@import "normalize.less"; +@import "print.less"; +@import "glyphicons.less"; + +// Core CSS +@import "scaffolding.less"; +@import "type.less"; +@import "code.less"; +@import "grid.less"; +@import "tables.less"; +@import "forms.less"; +@import "buttons.less"; + +// Components +@import "component-animations.less"; +@import "dropdowns.less"; +@import "button-groups.less"; +@import "input-groups.less"; +@import "navs.less"; +@import "navbar.less"; +@import "breadcrumbs.less"; +@import "pagination.less"; +@import "pager.less"; +@import "labels.less"; +@import "badges.less"; +@import "jumbotron.less"; +@import "thumbnails.less"; +@import "alerts.less"; +@import "progress-bars.less"; +@import "media.less"; +@import "list-group.less"; +@import "panels.less"; +@import "responsive-embed.less"; +@import "wells.less"; +@import "close.less"; + +// Components w/ JavaScript +@import "modals.less"; +@import "tooltip.less"; +@import "popovers.less"; +@import "carousel.less"; + +// Utility classes +@import "utilities.less"; +@import "responsive-utilities.less"; diff --git a/frontend/css/vendors/less/breadcrumbs.less b/frontend/css/vendors/less/breadcrumbs.less new file mode 100644 index 0000000..cb01d50 --- /dev/null +++ b/frontend/css/vendors/less/breadcrumbs.less @@ -0,0 +1,26 @@ +// +// Breadcrumbs +// -------------------------------------------------- + + +.breadcrumb { + padding: @breadcrumb-padding-vertical @breadcrumb-padding-horizontal; + margin-bottom: @line-height-computed; + list-style: none; + background-color: @breadcrumb-bg; + border-radius: @border-radius-base; + + > li { + display: inline-block; + + + li:before { + content: "@{breadcrumb-separator}\00a0"; // Unicode space added since inline-block means non-collapsing white-space + padding: 0 5px; + color: @breadcrumb-color; + } + } + + > .active { + color: @breadcrumb-active-color; + } +} diff --git a/frontend/css/vendors/less/button-groups.less b/frontend/css/vendors/less/button-groups.less new file mode 100644 index 0000000..293245a --- /dev/null +++ b/frontend/css/vendors/less/button-groups.less @@ -0,0 +1,244 @@ +// +// Button groups +// -------------------------------------------------- + +// Make the div behave like a button +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; // match .btn alignment given font-size hack above + > .btn { + position: relative; + float: left; + // Bring the "active" button to the front + &:hover, + &:focus, + &:active, + &.active { + z-index: 2; + } + } +} + +// Prevent double borders when buttons are next to each other +.btn-group { + .btn + .btn, + .btn + .btn-group, + .btn-group + .btn, + .btn-group + .btn-group { + margin-left: -1px; + } +} + +// Optional: Group multiple button groups together for a toolbar +.btn-toolbar { + margin-left: -5px; // Offset the first child's margin + &:extend(.clearfix all); + + .btn, + .btn-group, + .input-group { + float: left; + } + > .btn, + > .btn-group, + > .input-group { + margin-left: 5px; + } +} + +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} + +// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match +.btn-group > .btn:first-child { + margin-left: 0; + &:not(:last-child):not(.dropdown-toggle) { + .border-right-radius(0); + } +} +// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + .border-left-radius(0); +} + +// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group) +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child:not(:last-child) { + > .btn:last-child, + > .dropdown-toggle { + .border-right-radius(0); + } +} +.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { + .border-left-radius(0); +} + +// On active and open, don't show outline +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + + +// Sizing +// +// Remix the default button sizing classes into new ones for easier manipulation. + +.btn-group-xs > .btn { &:extend(.btn-xs); } +.btn-group-sm > .btn { &:extend(.btn-sm); } +.btn-group-lg > .btn { &:extend(.btn-lg); } + + +// Split button dropdowns +// ---------------------- + +// Give the line between buttons some depth +.btn-group > .btn + .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-left: 12px; + padding-right: 12px; +} + +// The clickable button for toggling the menu +// Remove the gradient and set the same inset shadow as the :active state +.btn-group.open .dropdown-toggle { + .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + + // Show no shadow for `.btn-link` since it has no other button styles. + &.btn-link { + .box-shadow(none); + } +} + + +// Reposition the caret +.btn .caret { + margin-left: 0; +} +// Carets in other button sizes +.btn-lg .caret { + border-width: @caret-width-large @caret-width-large 0; + border-bottom-width: 0; +} +// Upside down carets for .dropup +.dropup .btn-lg .caret { + border-width: 0 @caret-width-large @caret-width-large; +} + + +// Vertical button groups +// ---------------------- + +.btn-group-vertical { + > .btn, + > .btn-group, + > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; + } + + // Clear floats so dropdown menus can be properly placed + > .btn-group { + &:extend(.clearfix all); + > .btn { + float: none; + } + } + + > .btn + .btn, + > .btn + .btn-group, + > .btn-group + .btn, + > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; + } +} + +.btn-group-vertical > .btn { + &:not(:first-child):not(:last-child) { + border-radius: 0; + } + &:first-child:not(:last-child) { + .border-top-radius(@btn-border-radius-base); + .border-bottom-radius(0); + } + &:last-child:not(:first-child) { + .border-top-radius(0); + .border-bottom-radius(@btn-border-radius-base); + } +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) { + > .btn:last-child, + > .dropdown-toggle { + .border-bottom-radius(0); + } +} +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + .border-top-radius(0); +} + + +// Justified button groups +// ---------------------- + +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; + > .btn, + > .btn-group { + float: none; + display: table-cell; + width: 1%; + } + > .btn-group .btn { + width: 100%; + } + + > .btn-group .dropdown-menu { + left: auto; + } +} + + +// Checkbox and radio options +// +// In order to support the browser's form validation feedback, powered by the +// `required` attribute, we have to "hide" the inputs via `clip`. We cannot use +// `display: none;` or `visibility: hidden;` as that also hides the popover. +// Simply visually hiding the inputs via `opacity` would leave them clickable in +// certain cases which is prevented by using `clip` and `pointer-events`. +// This way, we ensure a DOM element is visible to position the popover from. +// +// See https://github.com/twbs/bootstrap/pull/12794 and +// https://github.com/twbs/bootstrap/pull/14559 for more information. + +[data-toggle="buttons"] { + > .btn, + > .btn-group > .btn { + input[type="radio"], + input[type="checkbox"] { + position: absolute; + clip: rect(0,0,0,0); + pointer-events: none; + } + } +} diff --git a/frontend/css/vendors/less/buttons.less b/frontend/css/vendors/less/buttons.less new file mode 100644 index 0000000..9cbb8f4 --- /dev/null +++ b/frontend/css/vendors/less/buttons.less @@ -0,0 +1,166 @@ +// +// Buttons +// -------------------------------------------------- + + +// Base styles +// -------------------------------------------------- + +.btn { + display: inline-block; + margin-bottom: 0; // For input.btn + font-weight: @btn-font-weight; + text-align: center; + vertical-align: middle; + touch-action: manipulation; + cursor: pointer; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid transparent; + white-space: nowrap; + .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @btn-border-radius-base); + .user-select(none); + + &, + &:active, + &.active { + &:focus, + &.focus { + .tab-focus(); + } + } + + &:hover, + &:focus, + &.focus { + color: @btn-default-color; + text-decoration: none; + } + + &:active, + &.active { + outline: 0; + background-image: none; + .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + } + + &.disabled, + &[disabled], + fieldset[disabled] & { + cursor: @cursor-disabled; + .opacity(.65); + .box-shadow(none); + } + + a& { + &.disabled, + fieldset[disabled] & { + pointer-events: none; // Future-proof disabling of clicks on `` elements + } + } +} + + +// Alternate buttons +// -------------------------------------------------- + +.btn-default { + .button-variant(@btn-default-color; @btn-default-bg; @btn-default-border); +} +.btn-primary { + .button-variant(@btn-primary-color; @btn-primary-bg; @btn-primary-border); +} +// Success appears as green +.btn-success { + .button-variant(@btn-success-color; @btn-success-bg; @btn-success-border); +} +// Info appears as blue-green +.btn-info { + .button-variant(@btn-info-color; @btn-info-bg; @btn-info-border); +} +// Warning appears as orange +.btn-warning { + .button-variant(@btn-warning-color; @btn-warning-bg; @btn-warning-border); +} +// Danger and error appear as red +.btn-danger { + .button-variant(@btn-danger-color; @btn-danger-bg; @btn-danger-border); +} + + +// Link buttons +// ------------------------- + +// Make a button look and behave like a link +.btn-link { + color: @link-color; + font-weight: normal; + border-radius: 0; + + &, + &:active, + &.active, + &[disabled], + fieldset[disabled] & { + background-color: transparent; + .box-shadow(none); + } + &, + &:hover, + &:focus, + &:active { + border-color: transparent; + } + &:hover, + &:focus { + color: @link-hover-color; + text-decoration: @link-hover-decoration; + background-color: transparent; + } + &[disabled], + fieldset[disabled] & { + &:hover, + &:focus { + color: @btn-link-disabled-color; + text-decoration: none; + } + } +} + + +// Button Sizes +// -------------------------------------------------- + +.btn-lg { + // line-height: ensure even-numbered height of button next to large input + .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @btn-border-radius-large); +} +.btn-sm { + // line-height: ensure proper height of button next to small input + .button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @btn-border-radius-small); +} +.btn-xs { + .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @btn-border-radius-small); +} + + +// Block button +// -------------------------------------------------- + +.btn-block { + display: block; + width: 100%; +} + +// Vertically space out multiple block buttons +.btn-block + .btn-block { + margin-top: 5px; +} + +// Specificity overrides +input[type="submit"], +input[type="reset"], +input[type="button"] { + &.btn-block { + width: 100%; + } +} diff --git a/frontend/css/vendors/less/carousel.less b/frontend/css/vendors/less/carousel.less new file mode 100644 index 0000000..252011e --- /dev/null +++ b/frontend/css/vendors/less/carousel.less @@ -0,0 +1,270 @@ +// +// Carousel +// -------------------------------------------------- + + +// Wrapper for the slide container and indicators +.carousel { + position: relative; +} + +.carousel-inner { + position: relative; + overflow: hidden; + width: 100%; + + > .item { + display: none; + position: relative; + .transition(.6s ease-in-out left); + + // Account for jankitude on images + > img, + > a > img { + &:extend(.img-responsive); + line-height: 1; + } + + // WebKit CSS3 transforms for supported devices + @media all and (transform-3d), (-webkit-transform-3d) { + .transition-transform(~'0.6s ease-in-out'); + .backface-visibility(~'hidden'); + .perspective(1000px); + + &.next, + &.active.right { + .translate3d(100%, 0, 0); + left: 0; + } + &.prev, + &.active.left { + .translate3d(-100%, 0, 0); + left: 0; + } + &.next.left, + &.prev.right, + &.active { + .translate3d(0, 0, 0); + left: 0; + } + } + } + + > .active, + > .next, + > .prev { + display: block; + } + + > .active { + left: 0; + } + + > .next, + > .prev { + position: absolute; + top: 0; + width: 100%; + } + + > .next { + left: 100%; + } + > .prev { + left: -100%; + } + > .next.left, + > .prev.right { + left: 0; + } + + > .active.left { + left: -100%; + } + > .active.right { + left: 100%; + } + +} + +// Left/right controls for nav +// --------------------------- + +.carousel-control { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: @carousel-control-width; + .opacity(@carousel-control-opacity); + font-size: @carousel-control-font-size; + color: @carousel-control-color; + text-align: center; + text-shadow: @carousel-text-shadow; + background-color: rgba(0, 0, 0, 0); // Fix IE9 click-thru bug + // We can't have this transition here because WebKit cancels the carousel + // animation if you trip this while in the middle of another animation. + + // Set gradients for backgrounds + &.left { + #gradient > .horizontal(@start-color: rgba(0,0,0,.5); @end-color: rgba(0,0,0,.0001)); + } + &.right { + left: auto; + right: 0; + #gradient > .horizontal(@start-color: rgba(0,0,0,.0001); @end-color: rgba(0,0,0,.5)); + } + + // Hover/focus state + &:hover, + &:focus { + outline: 0; + color: @carousel-control-color; + text-decoration: none; + .opacity(.9); + } + + // Toggles + .icon-prev, + .icon-next, + .glyphicon-chevron-left, + .glyphicon-chevron-right { + position: absolute; + top: 50%; + margin-top: -10px; + z-index: 5; + display: inline-block; + } + .icon-prev, + .glyphicon-chevron-left { + left: 50%; + margin-left: -10px; + } + .icon-next, + .glyphicon-chevron-right { + right: 50%; + margin-right: -10px; + } + .icon-prev, + .icon-next { + width: 20px; + height: 20px; + line-height: 1; + font-family: serif; + } + + + .icon-prev { + &:before { + content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039) + } + } + .icon-next { + &:before { + content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A) + } + } +} + +// Optional indicator pips +// +// Add an unordered list with the following class and add a list item for each +// slide your carousel holds. + +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + margin-left: -30%; + padding-left: 0; + list-style: none; + text-align: center; + + li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + border: 1px solid @carousel-indicator-border-color; + border-radius: 10px; + cursor: pointer; + + // IE8-9 hack for event handling + // + // Internet Explorer 8-9 does not support clicks on elements without a set + // `background-color`. We cannot use `filter` since that's not viewed as a + // background color by the browser. Thus, a hack is needed. + // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Internet_Explorer + // + // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we + // set alpha transparency for the best results possible. + background-color: #000 \9; // IE8 + background-color: rgba(0,0,0,0); // IE9 + } + .active { + margin: 0; + width: 12px; + height: 12px; + background-color: @carousel-indicator-active-bg; + } +} + +// Optional captions +// ----------------------------- +// Hidden by default for smaller viewports +.carousel-caption { + position: absolute; + left: 15%; + right: 15%; + bottom: 20px; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: @carousel-caption-color; + text-align: center; + text-shadow: @carousel-text-shadow; + & .btn { + text-shadow: none; // No shadow for button elements in carousel-caption + } +} + + +// Scale up controls for tablets and up +@media screen and (min-width: @screen-sm-min) { + + // Scale up the controls a smidge + .carousel-control { + .glyphicon-chevron-left, + .glyphicon-chevron-right, + .icon-prev, + .icon-next { + width: (@carousel-control-font-size * 1.5); + height: (@carousel-control-font-size * 1.5); + margin-top: (@carousel-control-font-size / -2); + font-size: (@carousel-control-font-size * 1.5); + } + .glyphicon-chevron-left, + .icon-prev { + margin-left: (@carousel-control-font-size / -2); + } + .glyphicon-chevron-right, + .icon-next { + margin-right: (@carousel-control-font-size / -2); + } + } + + // Show and left align the captions + .carousel-caption { + left: 20%; + right: 20%; + padding-bottom: 30px; + } + + // Move up the indicators + .carousel-indicators { + bottom: 20px; + } +} diff --git a/frontend/css/vendors/less/close.less b/frontend/css/vendors/less/close.less new file mode 100644 index 0000000..6d5bfe0 --- /dev/null +++ b/frontend/css/vendors/less/close.less @@ -0,0 +1,34 @@ +// +// Close icons +// -------------------------------------------------- + + +.close { + float: right; + font-size: (@font-size-base * 1.5); + font-weight: @close-font-weight; + line-height: 1; + color: @close-color; + text-shadow: @close-text-shadow; + .opacity(.2); + + &:hover, + &:focus { + color: @close-color; + text-decoration: none; + cursor: pointer; + .opacity(.5); + } + + // Additional properties for button version + // iOS requires the button element instead of an anchor tag. + // If you want the anchor version, it requires `href="#"`. + // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile + button& { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; + } +} diff --git a/frontend/css/vendors/less/code.less b/frontend/css/vendors/less/code.less new file mode 100644 index 0000000..a08b4d4 --- /dev/null +++ b/frontend/css/vendors/less/code.less @@ -0,0 +1,69 @@ +// +// Code (inline and block) +// -------------------------------------------------- + + +// Inline and block code styles +code, +kbd, +pre, +samp { + font-family: @font-family-monospace; +} + +// Inline code +code { + padding: 2px 4px; + font-size: 90%; + color: @code-color; + background-color: @code-bg; + border-radius: @border-radius-base; +} + +// User input typically entered via keyboard +kbd { + padding: 2px 4px; + font-size: 90%; + color: @kbd-color; + background-color: @kbd-bg; + border-radius: @border-radius-small; + box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); + + kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + box-shadow: none; + } +} + +// Blocks of code +pre { + display: block; + padding: ((@line-height-computed - 1) / 2); + margin: 0 0 (@line-height-computed / 2); + font-size: (@font-size-base - 1); // 14px to 13px + line-height: @line-height-base; + word-break: break-all; + word-wrap: break-word; + color: @pre-color; + background-color: @pre-bg; + border: 1px solid @pre-border-color; + border-radius: @border-radius-base; + + // Account for some code outputs that place code tags in pre tags + code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; + } +} + +// Enable scrollable blocks of code +.pre-scrollable { + max-height: @pre-scrollable-max-height; + overflow-y: scroll; +} diff --git a/frontend/css/vendors/less/component-animations.less b/frontend/css/vendors/less/component-animations.less new file mode 100644 index 0000000..0bcee91 --- /dev/null +++ b/frontend/css/vendors/less/component-animations.less @@ -0,0 +1,33 @@ +// +// Component animations +// -------------------------------------------------- + +// Heads up! +// +// We don't use the `.opacity()` mixin here since it causes a bug with text +// fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552. + +.fade { + opacity: 0; + .transition(opacity .15s linear); + &.in { + opacity: 1; + } +} + +.collapse { + display: none; + + &.in { display: block; } + tr&.in { display: table-row; } + tbody&.in { display: table-row-group; } +} + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + .transition-property(~"height, visibility"); + .transition-duration(.35s); + .transition-timing-function(ease); +} diff --git a/frontend/css/vendors/less/dropdowns.less b/frontend/css/vendors/less/dropdowns.less new file mode 100644 index 0000000..f6876c1 --- /dev/null +++ b/frontend/css/vendors/less/dropdowns.less @@ -0,0 +1,216 @@ +// +// Dropdown menus +// -------------------------------------------------- + + +// Dropdown arrow/caret +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: @caret-width-base dashed; + border-top: @caret-width-base solid ~"\9"; // IE8 + border-right: @caret-width-base solid transparent; + border-left: @caret-width-base solid transparent; +} + +// The dropdown wrapper (div) +.dropup, +.dropdown { + position: relative; +} + +// Prevent the focus on the dropdown toggle when closing dropdowns +.dropdown-toggle:focus { + outline: 0; +} + +// The dropdown menu (ul) +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: @zindex-dropdown; + display: none; // none by default, but block on "open" of the menu + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; // override default ul + list-style: none; + font-size: @font-size-base; + text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer) + background-color: @dropdown-bg; + border: 1px solid @dropdown-fallback-border; // IE8 fallback + border: 1px solid @dropdown-border; + border-radius: @border-radius-base; + .box-shadow(0 6px 12px rgba(0,0,0,.175)); + background-clip: padding-box; + + // Aligns the dropdown menu to right + // + // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]` + &.pull-right { + right: 0; + left: auto; + } + + // Dividers (basically an hr) within the dropdown + .divider { + .nav-divider(@dropdown-divider-bg); + } + + // Links within the dropdown menu + > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: @line-height-base; + color: @dropdown-link-color; + white-space: nowrap; // prevent links from randomly breaking onto new lines + } +} + +// Hover/Focus state +.dropdown-menu > li > a { + &:hover, + &:focus { + text-decoration: none; + color: @dropdown-link-hover-color; + background-color: @dropdown-link-hover-bg; + } +} + +// Active state +.dropdown-menu > .active > a { + &, + &:hover, + &:focus { + color: @dropdown-link-active-color; + text-decoration: none; + outline: 0; + background-color: @dropdown-link-active-bg; + } +} + +// Disabled state +// +// Gray out text and ensure the hover/focus state remains gray + +.dropdown-menu > .disabled > a { + &, + &:hover, + &:focus { + color: @dropdown-link-disabled-color; + } + + // Nuke hover/focus effects + &:hover, + &:focus { + text-decoration: none; + background-color: transparent; + background-image: none; // Remove CSS gradient + .reset-filter(); + cursor: @cursor-disabled; + } +} + +// Open state for the dropdown +.open { + // Show the menu + > .dropdown-menu { + display: block; + } + + // Remove the outline when :focus is triggered + > a { + outline: 0; + } +} + +// Menu positioning +// +// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown +// menu with the parent. +.dropdown-menu-right { + left: auto; // Reset the default from `.dropdown-menu` + right: 0; +} +// With v3, we enabled auto-flipping if you have a dropdown within a right +// aligned nav component. To enable the undoing of that, we provide an override +// to restore the default dropdown menu alignment. +// +// This is only for left-aligning a dropdown menu within a `.navbar-right` or +// `.pull-right` nav component. +.dropdown-menu-left { + left: 0; + right: auto; +} + +// Dropdown section headers +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: @font-size-small; + line-height: @line-height-base; + color: @dropdown-header-color; + white-space: nowrap; // as with > li > a +} + +// Backdrop to catch body clicks on mobile, etc. +.dropdown-backdrop { + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: (@zindex-dropdown - 10); +} + +// Right aligned dropdowns +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +// Allow for dropdowns to go bottom up (aka, dropup-menu) +// +// Just add .dropup after the standard .dropdown class and you're set, bro. +// TODO: abstract this so that the navbar fixed styles are not placed here? + +.dropup, +.navbar-fixed-bottom .dropdown { + // Reverse the caret + .caret { + border-top: 0; + border-bottom: @caret-width-base dashed; + border-bottom: @caret-width-base solid ~"\9"; // IE8 + content: ""; + } + // Different positioning for bottom up menu + .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 2px; + } +} + + +// Component alignment +// +// Reiterate per navbar.less and the modified component alignment there. + +@media (min-width: @grid-float-breakpoint) { + .navbar-right { + .dropdown-menu { + .dropdown-menu-right(); + } + // Necessary for overrides of the default right aligned menu. + // Will remove come v4 in all likelihood. + .dropdown-menu-left { + .dropdown-menu-left(); + } + } +} diff --git a/frontend/css/vendors/less/forms.less b/frontend/css/vendors/less/forms.less new file mode 100644 index 0000000..e8b071a --- /dev/null +++ b/frontend/css/vendors/less/forms.less @@ -0,0 +1,613 @@ +// +// Forms +// -------------------------------------------------- + + +// Normalize non-controls +// +// Restyle and baseline non-control form elements. + +fieldset { + padding: 0; + margin: 0; + border: 0; + // Chrome and Firefox set a `min-width: min-content;` on fieldsets, + // so we reset that to ensure it behaves more like a standard block element. + // See https://github.com/twbs/bootstrap/issues/12359. + min-width: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: @line-height-computed; + font-size: (@font-size-base * 1.5); + line-height: inherit; + color: @legend-color; + border: 0; + border-bottom: 1px solid @legend-border-color; +} + +label { + display: inline-block; + max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141) + margin-bottom: 5px; + font-weight: bold; +} + + +// Normalize form controls +// +// While most of our form styles require extra classes, some basic normalization +// is required to ensure optimum display with or without those classes to better +// address browser inconsistencies. + +// Override content-box in Normalize (* isn't specific enough) +input[type="search"] { + .box-sizing(border-box); +} + +// Position radios and checkboxes better +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; // IE8-9 + line-height: normal; +} + +input[type="file"] { + display: block; +} + +// Make range inputs behave like textual form controls +input[type="range"] { + display: block; + width: 100%; +} + +// Make multiple select elements height not fixed +select[multiple], +select[size] { + height: auto; +} + +// Focus for file, radio, and checkbox +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + .tab-focus(); +} + +// Adjust output element +output { + display: block; + padding-top: (@padding-base-vertical + 1); + font-size: @font-size-base; + line-height: @line-height-base; + color: @input-color; +} + + +// Common form controls +// +// Shared size and type resets for form controls. Apply `.form-control` to any +// of the following form controls: +// +// select +// textarea +// input[type="text"] +// input[type="password"] +// input[type="datetime"] +// input[type="datetime-local"] +// input[type="date"] +// input[type="month"] +// input[type="time"] +// input[type="week"] +// input[type="number"] +// input[type="email"] +// input[type="url"] +// input[type="search"] +// input[type="tel"] +// input[type="color"] + +.form-control { + display: block; + width: 100%; + height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border) + padding: @padding-base-vertical @padding-base-horizontal; + font-size: @font-size-base; + line-height: @line-height-base; + color: @input-color; + background-color: @input-bg; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid @input-border; + border-radius: @input-border-radius; // Note: This has no effect on s in CSS. + .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); + .transition(~"border-color ease-in-out .15s, box-shadow ease-in-out .15s"); + + // Customize the `:focus` state to imitate native WebKit styles. + .form-control-focus(); + + // Placeholder + .placeholder(); + + // Unstyle the caret on `` +// element gets special love because it's special, and that's a fact! +.input-size(@input-height; @padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) { + height: @input-height; + padding: @padding-vertical @padding-horizontal; + font-size: @font-size; + line-height: @line-height; + border-radius: @border-radius; + + select& { + height: @input-height; + line-height: @input-height; + } + + textarea&, + select[multiple]& { + height: auto; + } +} diff --git a/frontend/css/vendors/less/mixins/gradients.less b/frontend/css/vendors/less/mixins/gradients.less new file mode 100644 index 0000000..0b88a89 --- /dev/null +++ b/frontend/css/vendors/less/mixins/gradients.less @@ -0,0 +1,59 @@ +// Gradients + +#gradient { + + // Horizontal gradient, from left to right + // + // Creates two color stops, start and end, by specifying a color and position for each color stop. + // Color stops are not available in IE9 and below. + .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) { + background-image: -webkit-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+ + background-image: -o-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Opera 12 + background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ + background-repeat: repeat-x; + filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",argb(@start-color),argb(@end-color))); // IE9 and down + } + + // Vertical gradient, from top to bottom + // + // Creates two color stops, start and end, by specifying a color and position for each color stop. + // Color stops are not available in IE9 and below. + .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) { + background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+ + background-image: -o-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Opera 12 + background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ + background-repeat: repeat-x; + filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@start-color),argb(@end-color))); // IE9 and down + } + + .directional(@start-color: #555; @end-color: #333; @deg: 45deg) { + background-repeat: repeat-x; + background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+ + background-image: -o-linear-gradient(@deg, @start-color, @end-color); // Opera 12 + background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ + } + .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) { + background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color); + background-image: -o-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color); + background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color); + background-repeat: no-repeat; + filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback + } + .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) { + background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color); + background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color); + background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color); + background-repeat: no-repeat; + filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback + } + .radial(@inner-color: #555; @outer-color: #333) { + background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color); + background-image: radial-gradient(circle, @inner-color, @outer-color); + background-repeat: no-repeat; + } + .striped(@color: rgba(255,255,255,.15); @angle: 45deg) { + background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent); + background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent); + } +} diff --git a/frontend/css/vendors/less/mixins/grid-framework.less b/frontend/css/vendors/less/mixins/grid-framework.less new file mode 100644 index 0000000..8c23eed --- /dev/null +++ b/frontend/css/vendors/less/mixins/grid-framework.less @@ -0,0 +1,91 @@ +// Framework grid generation +// +// Used only by Bootstrap to generate the correct number of grid classes given +// any value of `@grid-columns`. + +.make-grid-columns() { + // Common styles for all sizes of grid columns, widths 1-12 + .col(@index) { // initial + @item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}"; + .col((@index + 1), @item); + } + .col(@index, @list) when (@index =< @grid-columns) { // general; "=<" isn't a typo + @item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}"; + .col((@index + 1), ~"@{list}, @{item}"); + } + .col(@index, @list) when (@index > @grid-columns) { // terminal + @{list} { + position: relative; + // Prevent columns from collapsing when empty + min-height: 1px; + // Inner gutter via padding + padding-left: ceil((@grid-gutter-width / 2)); + padding-right: floor((@grid-gutter-width / 2)); + } + } + .col(1); // kickstart it +} + +.float-grid-columns(@class) { + .col(@index) { // initial + @item: ~".col-@{class}-@{index}"; + .col((@index + 1), @item); + } + .col(@index, @list) when (@index =< @grid-columns) { // general + @item: ~".col-@{class}-@{index}"; + .col((@index + 1), ~"@{list}, @{item}"); + } + .col(@index, @list) when (@index > @grid-columns) { // terminal + @{list} { + float: left; + } + } + .col(1); // kickstart it +} + +.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) { + .col-@{class}-@{index} { + width: percentage((@index / @grid-columns)); + } +} +.calc-grid-column(@index, @class, @type) when (@type = push) and (@index > 0) { + .col-@{class}-push-@{index} { + left: percentage((@index / @grid-columns)); + } +} +.calc-grid-column(@index, @class, @type) when (@type = push) and (@index = 0) { + .col-@{class}-push-0 { + left: auto; + } +} +.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index > 0) { + .col-@{class}-pull-@{index} { + right: percentage((@index / @grid-columns)); + } +} +.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index = 0) { + .col-@{class}-pull-0 { + right: auto; + } +} +.calc-grid-column(@index, @class, @type) when (@type = offset) { + .col-@{class}-offset-@{index} { + margin-left: percentage((@index / @grid-columns)); + } +} + +// Basic looping in LESS +.loop-grid-columns(@index, @class, @type) when (@index >= 0) { + .calc-grid-column(@index, @class, @type); + // next iteration + .loop-grid-columns((@index - 1), @class, @type); +} + +// Create grid for specific class +.make-grid(@class) { + .float-grid-columns(@class); + .loop-grid-columns(@grid-columns, @class, width); + .loop-grid-columns(@grid-columns, @class, pull); + .loop-grid-columns(@grid-columns, @class, push); + .loop-grid-columns(@grid-columns, @class, offset); +} diff --git a/frontend/css/vendors/less/mixins/grid.less b/frontend/css/vendors/less/mixins/grid.less new file mode 100644 index 0000000..df496d0 --- /dev/null +++ b/frontend/css/vendors/less/mixins/grid.less @@ -0,0 +1,122 @@ +// Grid system +// +// Generate semantic grid columns with these mixins. + +// Centered container element +.container-fixed(@gutter: @grid-gutter-width) { + margin-right: auto; + margin-left: auto; + padding-left: floor((@gutter / 2)); + padding-right: ceil((@gutter / 2)); + &:extend(.clearfix all); +} + +// Creates a wrapper for a series of columns +.make-row(@gutter: @grid-gutter-width) { + margin-left: ceil((@gutter / -2)); + margin-right: floor((@gutter / -2)); + &:extend(.clearfix all); +} + +// Generate the extra small columns +.make-xs-column(@columns; @gutter: @grid-gutter-width) { + position: relative; + float: left; + width: percentage((@columns / @grid-columns)); + min-height: 1px; + padding-left: (@gutter / 2); + padding-right: (@gutter / 2); +} +.make-xs-column-offset(@columns) { + margin-left: percentage((@columns / @grid-columns)); +} +.make-xs-column-push(@columns) { + left: percentage((@columns / @grid-columns)); +} +.make-xs-column-pull(@columns) { + right: percentage((@columns / @grid-columns)); +} + +// Generate the small columns +.make-sm-column(@columns; @gutter: @grid-gutter-width) { + position: relative; + min-height: 1px; + padding-left: (@gutter / 2); + padding-right: (@gutter / 2); + + @media (min-width: @screen-sm-min) { + float: left; + width: percentage((@columns / @grid-columns)); + } +} +.make-sm-column-offset(@columns) { + @media (min-width: @screen-sm-min) { + margin-left: percentage((@columns / @grid-columns)); + } +} +.make-sm-column-push(@columns) { + @media (min-width: @screen-sm-min) { + left: percentage((@columns / @grid-columns)); + } +} +.make-sm-column-pull(@columns) { + @media (min-width: @screen-sm-min) { + right: percentage((@columns / @grid-columns)); + } +} + +// Generate the medium columns +.make-md-column(@columns; @gutter: @grid-gutter-width) { + position: relative; + min-height: 1px; + padding-left: (@gutter / 2); + padding-right: (@gutter / 2); + + @media (min-width: @screen-md-min) { + float: left; + width: percentage((@columns / @grid-columns)); + } +} +.make-md-column-offset(@columns) { + @media (min-width: @screen-md-min) { + margin-left: percentage((@columns / @grid-columns)); + } +} +.make-md-column-push(@columns) { + @media (min-width: @screen-md-min) { + left: percentage((@columns / @grid-columns)); + } +} +.make-md-column-pull(@columns) { + @media (min-width: @screen-md-min) { + right: percentage((@columns / @grid-columns)); + } +} + +// Generate the large columns +.make-lg-column(@columns; @gutter: @grid-gutter-width) { + position: relative; + min-height: 1px; + padding-left: (@gutter / 2); + padding-right: (@gutter / 2); + + @media (min-width: @screen-lg-min) { + float: left; + width: percentage((@columns / @grid-columns)); + } +} +.make-lg-column-offset(@columns) { + @media (min-width: @screen-lg-min) { + margin-left: percentage((@columns / @grid-columns)); + } +} +.make-lg-column-push(@columns) { + @media (min-width: @screen-lg-min) { + left: percentage((@columns / @grid-columns)); + } +} +.make-lg-column-pull(@columns) { + @media (min-width: @screen-lg-min) { + right: percentage((@columns / @grid-columns)); + } +} diff --git a/frontend/css/vendors/less/mixins/hide-text.less b/frontend/css/vendors/less/mixins/hide-text.less new file mode 100644 index 0000000..2bb84a3 --- /dev/null +++ b/frontend/css/vendors/less/mixins/hide-text.less @@ -0,0 +1,21 @@ +// CSS image replacement +// +// Heads up! v3 launched with only `.hide-text()`, but per our pattern for +// mixins being reused as classes with the same name, this doesn't hold up. As +// of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`. +// +// Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757 + +// Deprecated as of v3.0.1 (has been removed in v4) +.hide-text() { + font: ~"0/0" a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +// New mixin to use as of v3.0.1 +.text-hide() { + .hide-text(); +} diff --git a/frontend/css/vendors/less/mixins/image.less b/frontend/css/vendors/less/mixins/image.less new file mode 100644 index 0000000..f233cb3 --- /dev/null +++ b/frontend/css/vendors/less/mixins/image.less @@ -0,0 +1,33 @@ +// Image Mixins +// - Responsive image +// - Retina image + + +// Responsive image +// +// Keep images from scaling beyond the width of their parents. +.img-responsive(@display: block) { + display: @display; + max-width: 100%; // Part 1: Set a maximum relative to the parent + height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching +} + + +// Retina image +// +// Short retina mixin for setting background-image and -size. Note that the +// spelling of `min--moz-device-pixel-ratio` is intentional. +.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) { + background-image: url("@{file-1x}"); + + @media + only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and ( min--moz-device-pixel-ratio: 2), + only screen and ( -o-min-device-pixel-ratio: 2/1), + only screen and ( min-device-pixel-ratio: 2), + only screen and ( min-resolution: 192dpi), + only screen and ( min-resolution: 2dppx) { + background-image: url("@{file-2x}"); + background-size: @width-1x @height-1x; + } +} diff --git a/frontend/css/vendors/less/mixins/labels.less b/frontend/css/vendors/less/mixins/labels.less new file mode 100644 index 0000000..9f7a67e --- /dev/null +++ b/frontend/css/vendors/less/mixins/labels.less @@ -0,0 +1,12 @@ +// Labels + +.label-variant(@color) { + background-color: @color; + + &[href] { + &:hover, + &:focus { + background-color: darken(@color, 10%); + } + } +} diff --git a/frontend/css/vendors/less/mixins/list-group.less b/frontend/css/vendors/less/mixins/list-group.less new file mode 100644 index 0000000..03aa190 --- /dev/null +++ b/frontend/css/vendors/less/mixins/list-group.less @@ -0,0 +1,30 @@ +// List Groups + +.list-group-item-variant(@state; @background; @color) { + .list-group-item-@{state} { + color: @color; + background-color: @background; + + a&, + button& { + color: @color; + + .list-group-item-heading { + color: inherit; + } + + &:hover, + &:focus { + color: @color; + background-color: darken(@background, 5%); + } + &.active, + &.active:hover, + &.active:focus { + color: #fff; + background-color: @color; + border-color: @color; + } + } + } +} diff --git a/frontend/css/vendors/less/mixins/nav-divider.less b/frontend/css/vendors/less/mixins/nav-divider.less new file mode 100644 index 0000000..feb1e9e --- /dev/null +++ b/frontend/css/vendors/less/mixins/nav-divider.less @@ -0,0 +1,10 @@ +// Horizontal dividers +// +// Dividers (basically an hr) within dropdowns and nav lists + +.nav-divider(@color: #e5e5e5) { + height: 1px; + margin: ((@line-height-computed / 2) - 1) 0; + overflow: hidden; + background-color: @color; +} diff --git a/frontend/css/vendors/less/mixins/nav-vertical-align.less b/frontend/css/vendors/less/mixins/nav-vertical-align.less new file mode 100644 index 0000000..d458c78 --- /dev/null +++ b/frontend/css/vendors/less/mixins/nav-vertical-align.less @@ -0,0 +1,9 @@ +// Navbar vertical align +// +// Vertically center elements in the navbar. +// Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin. + +.navbar-vertical-align(@element-height) { + margin-top: ((@navbar-height - @element-height) / 2); + margin-bottom: ((@navbar-height - @element-height) / 2); +} diff --git a/frontend/css/vendors/less/mixins/opacity.less b/frontend/css/vendors/less/mixins/opacity.less new file mode 100644 index 0000000..33ed25c --- /dev/null +++ b/frontend/css/vendors/less/mixins/opacity.less @@ -0,0 +1,8 @@ +// Opacity + +.opacity(@opacity) { + opacity: @opacity; + // IE8 filter + @opacity-ie: (@opacity * 100); + filter: ~"alpha(opacity=@{opacity-ie})"; +} diff --git a/frontend/css/vendors/less/mixins/pagination.less b/frontend/css/vendors/less/mixins/pagination.less new file mode 100644 index 0000000..618804f --- /dev/null +++ b/frontend/css/vendors/less/mixins/pagination.less @@ -0,0 +1,24 @@ +// Pagination + +.pagination-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) { + > li { + > a, + > span { + padding: @padding-vertical @padding-horizontal; + font-size: @font-size; + line-height: @line-height; + } + &:first-child { + > a, + > span { + .border-left-radius(@border-radius); + } + } + &:last-child { + > a, + > span { + .border-right-radius(@border-radius); + } + } + } +} diff --git a/frontend/css/vendors/less/mixins/panels.less b/frontend/css/vendors/less/mixins/panels.less new file mode 100644 index 0000000..49ee10d --- /dev/null +++ b/frontend/css/vendors/less/mixins/panels.less @@ -0,0 +1,24 @@ +// Panels + +.panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) { + border-color: @border; + + & > .panel-heading { + color: @heading-text-color; + background-color: @heading-bg-color; + border-color: @heading-border; + + + .panel-collapse > .panel-body { + border-top-color: @border; + } + .badge { + color: @heading-bg-color; + background-color: @heading-text-color; + } + } + & > .panel-footer { + + .panel-collapse > .panel-body { + border-bottom-color: @border; + } + } +} diff --git a/frontend/css/vendors/less/mixins/progress-bar.less b/frontend/css/vendors/less/mixins/progress-bar.less new file mode 100644 index 0000000..f07996a --- /dev/null +++ b/frontend/css/vendors/less/mixins/progress-bar.less @@ -0,0 +1,10 @@ +// Progress bars + +.progress-bar-variant(@color) { + background-color: @color; + + // Deprecated parent class requirement as of v3.2.0 + .progress-striped & { + #gradient > .striped(); + } +} diff --git a/frontend/css/vendors/less/mixins/reset-filter.less b/frontend/css/vendors/less/mixins/reset-filter.less new file mode 100644 index 0000000..68cdb5e --- /dev/null +++ b/frontend/css/vendors/less/mixins/reset-filter.less @@ -0,0 +1,8 @@ +// Reset filters for IE +// +// When you need to remove a gradient background, do not forget to use this to reset +// the IE filter for IE9 and below. + +.reset-filter() { + filter: e(%("progid:DXImageTransform.Microsoft.gradient(enabled = false)")); +} diff --git a/frontend/css/vendors/less/mixins/reset-text.less b/frontend/css/vendors/less/mixins/reset-text.less new file mode 100644 index 0000000..58dd4d1 --- /dev/null +++ b/frontend/css/vendors/less/mixins/reset-text.less @@ -0,0 +1,18 @@ +.reset-text() { + font-family: @font-family-base; + // We deliberately do NOT reset font-size. + font-style: normal; + font-weight: normal; + letter-spacing: normal; + line-break: auto; + line-height: @line-height-base; + text-align: left; // Fallback for where `start` is not supported + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + white-space: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; +} diff --git a/frontend/css/vendors/less/mixins/resize.less b/frontend/css/vendors/less/mixins/resize.less new file mode 100644 index 0000000..3acd3af --- /dev/null +++ b/frontend/css/vendors/less/mixins/resize.less @@ -0,0 +1,6 @@ +// Resize anything + +.resizable(@direction) { + resize: @direction; // Options: horizontal, vertical, both + overflow: auto; // Per CSS3 UI, `resize` only applies when `overflow` isn't `visible` +} diff --git a/frontend/css/vendors/less/mixins/responsive-visibility.less b/frontend/css/vendors/less/mixins/responsive-visibility.less new file mode 100644 index 0000000..ecf1e97 --- /dev/null +++ b/frontend/css/vendors/less/mixins/responsive-visibility.less @@ -0,0 +1,15 @@ +// Responsive utilities + +// +// More easily include all the states for responsive-utilities.less. +.responsive-visibility() { + display: block !important; + table& { display: table !important; } + tr& { display: table-row !important; } + th&, + td& { display: table-cell !important; } +} + +.responsive-invisibility() { + display: none !important; +} diff --git a/frontend/css/vendors/less/mixins/size.less b/frontend/css/vendors/less/mixins/size.less new file mode 100644 index 0000000..a8be650 --- /dev/null +++ b/frontend/css/vendors/less/mixins/size.less @@ -0,0 +1,10 @@ +// Sizing shortcuts + +.size(@width; @height) { + width: @width; + height: @height; +} + +.square(@size) { + .size(@size; @size); +} diff --git a/frontend/css/vendors/less/mixins/tab-focus.less b/frontend/css/vendors/less/mixins/tab-focus.less new file mode 100644 index 0000000..1f1f05a --- /dev/null +++ b/frontend/css/vendors/less/mixins/tab-focus.less @@ -0,0 +1,9 @@ +// WebKit-style focus + +.tab-focus() { + // Default + outline: thin dotted; + // WebKit + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} diff --git a/frontend/css/vendors/less/mixins/table-row.less b/frontend/css/vendors/less/mixins/table-row.less new file mode 100644 index 0000000..0f287f1 --- /dev/null +++ b/frontend/css/vendors/less/mixins/table-row.less @@ -0,0 +1,28 @@ +// Tables + +.table-row-variant(@state; @background) { + // Exact selectors below required to override `.table-striped` and prevent + // inheritance to nested tables. + .table > thead > tr, + .table > tbody > tr, + .table > tfoot > tr { + > td.@{state}, + > th.@{state}, + &.@{state} > td, + &.@{state} > th { + background-color: @background; + } + } + + // Hover states for `.table-hover` + // Note: this is not available for cells or rows within `thead` or `tfoot`. + .table-hover > tbody > tr { + > td.@{state}:hover, + > th.@{state}:hover, + &.@{state}:hover > td, + &:hover > .@{state}, + &.@{state}:hover > th { + background-color: darken(@background, 5%); + } + } +} diff --git a/frontend/css/vendors/less/mixins/text-emphasis.less b/frontend/css/vendors/less/mixins/text-emphasis.less new file mode 100644 index 0000000..9e8a77a --- /dev/null +++ b/frontend/css/vendors/less/mixins/text-emphasis.less @@ -0,0 +1,9 @@ +// Typography + +.text-emphasis-variant(@color) { + color: @color; + a&:hover, + a&:focus { + color: darken(@color, 10%); + } +} diff --git a/frontend/css/vendors/less/mixins/text-overflow.less b/frontend/css/vendors/less/mixins/text-overflow.less new file mode 100644 index 0000000..c11ad2f --- /dev/null +++ b/frontend/css/vendors/less/mixins/text-overflow.less @@ -0,0 +1,8 @@ +// Text overflow +// Requires inline-block or block for proper styling + +.text-overflow() { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/frontend/css/vendors/less/mixins/vendor-prefixes.less b/frontend/css/vendors/less/mixins/vendor-prefixes.less new file mode 100644 index 0000000..2b5e74b --- /dev/null +++ b/frontend/css/vendors/less/mixins/vendor-prefixes.less @@ -0,0 +1,227 @@ +// Vendor Prefixes +// +// All vendor mixins are deprecated as of v3.2.0 due to the introduction of +// Autoprefixer in our Gruntfile. They have been removed in v4. + +// - Animations +// - Backface visibility +// - Box shadow +// - Box sizing +// - Content columns +// - Hyphens +// - Placeholder text +// - Transformations +// - Transitions +// - User Select + + +// Animations +.animation(@animation) { + -webkit-animation: @animation; + -o-animation: @animation; + animation: @animation; +} +.animation-name(@name) { + -webkit-animation-name: @name; + animation-name: @name; +} +.animation-duration(@duration) { + -webkit-animation-duration: @duration; + animation-duration: @duration; +} +.animation-timing-function(@timing-function) { + -webkit-animation-timing-function: @timing-function; + animation-timing-function: @timing-function; +} +.animation-delay(@delay) { + -webkit-animation-delay: @delay; + animation-delay: @delay; +} +.animation-iteration-count(@iteration-count) { + -webkit-animation-iteration-count: @iteration-count; + animation-iteration-count: @iteration-count; +} +.animation-direction(@direction) { + -webkit-animation-direction: @direction; + animation-direction: @direction; +} +.animation-fill-mode(@fill-mode) { + -webkit-animation-fill-mode: @fill-mode; + animation-fill-mode: @fill-mode; +} + +// Backface visibility +// Prevent browsers from flickering when using CSS 3D transforms. +// Default value is `visible`, but can be changed to `hidden` + +.backface-visibility(@visibility) { + -webkit-backface-visibility: @visibility; + -moz-backface-visibility: @visibility; + backface-visibility: @visibility; +} + +// Drop shadows +// +// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's +// supported browsers that have box shadow capabilities now support it. + +.box-shadow(@shadow) { + -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1 + box-shadow: @shadow; +} + +// Box sizing +.box-sizing(@boxmodel) { + -webkit-box-sizing: @boxmodel; + -moz-box-sizing: @boxmodel; + box-sizing: @boxmodel; +} + +// CSS3 Content Columns +.content-columns(@column-count; @column-gap: @grid-gutter-width) { + -webkit-column-count: @column-count; + -moz-column-count: @column-count; + column-count: @column-count; + -webkit-column-gap: @column-gap; + -moz-column-gap: @column-gap; + column-gap: @column-gap; +} + +// Optional hyphenation +.hyphens(@mode: auto) { + word-wrap: break-word; + -webkit-hyphens: @mode; + -moz-hyphens: @mode; + -ms-hyphens: @mode; // IE10+ + -o-hyphens: @mode; + hyphens: @mode; +} + +// Placeholder text +.placeholder(@color: @input-color-placeholder) { + // Firefox + &::-moz-placeholder { + color: @color; + opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526 + } + &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+ + &::-webkit-input-placeholder { color: @color; } // Safari and Chrome +} + +// Transformations +.scale(@ratio) { + -webkit-transform: scale(@ratio); + -ms-transform: scale(@ratio); // IE9 only + -o-transform: scale(@ratio); + transform: scale(@ratio); +} +.scale(@ratioX; @ratioY) { + -webkit-transform: scale(@ratioX, @ratioY); + -ms-transform: scale(@ratioX, @ratioY); // IE9 only + -o-transform: scale(@ratioX, @ratioY); + transform: scale(@ratioX, @ratioY); +} +.scaleX(@ratio) { + -webkit-transform: scaleX(@ratio); + -ms-transform: scaleX(@ratio); // IE9 only + -o-transform: scaleX(@ratio); + transform: scaleX(@ratio); +} +.scaleY(@ratio) { + -webkit-transform: scaleY(@ratio); + -ms-transform: scaleY(@ratio); // IE9 only + -o-transform: scaleY(@ratio); + transform: scaleY(@ratio); +} +.skew(@x; @y) { + -webkit-transform: skewX(@x) skewY(@y); + -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+ + -o-transform: skewX(@x) skewY(@y); + transform: skewX(@x) skewY(@y); +} +.translate(@x; @y) { + -webkit-transform: translate(@x, @y); + -ms-transform: translate(@x, @y); // IE9 only + -o-transform: translate(@x, @y); + transform: translate(@x, @y); +} +.translate3d(@x; @y; @z) { + -webkit-transform: translate3d(@x, @y, @z); + transform: translate3d(@x, @y, @z); +} +.rotate(@degrees) { + -webkit-transform: rotate(@degrees); + -ms-transform: rotate(@degrees); // IE9 only + -o-transform: rotate(@degrees); + transform: rotate(@degrees); +} +.rotateX(@degrees) { + -webkit-transform: rotateX(@degrees); + -ms-transform: rotateX(@degrees); // IE9 only + -o-transform: rotateX(@degrees); + transform: rotateX(@degrees); +} +.rotateY(@degrees) { + -webkit-transform: rotateY(@degrees); + -ms-transform: rotateY(@degrees); // IE9 only + -o-transform: rotateY(@degrees); + transform: rotateY(@degrees); +} +.perspective(@perspective) { + -webkit-perspective: @perspective; + -moz-perspective: @perspective; + perspective: @perspective; +} +.perspective-origin(@perspective) { + -webkit-perspective-origin: @perspective; + -moz-perspective-origin: @perspective; + perspective-origin: @perspective; +} +.transform-origin(@origin) { + -webkit-transform-origin: @origin; + -moz-transform-origin: @origin; + -ms-transform-origin: @origin; // IE9 only + transform-origin: @origin; +} + + +// Transitions + +.transition(@transition) { + -webkit-transition: @transition; + -o-transition: @transition; + transition: @transition; +} +.transition-property(@transition-property) { + -webkit-transition-property: @transition-property; + transition-property: @transition-property; +} +.transition-delay(@transition-delay) { + -webkit-transition-delay: @transition-delay; + transition-delay: @transition-delay; +} +.transition-duration(@transition-duration) { + -webkit-transition-duration: @transition-duration; + transition-duration: @transition-duration; +} +.transition-timing-function(@timing-function) { + -webkit-transition-timing-function: @timing-function; + transition-timing-function: @timing-function; +} +.transition-transform(@transition) { + -webkit-transition: -webkit-transform @transition; + -moz-transition: -moz-transform @transition; + -o-transition: -o-transform @transition; + transition: transform @transition; +} + + +// User select +// For selecting text on the page + +.user-select(@select) { + -webkit-user-select: @select; + -moz-user-select: @select; + -ms-user-select: @select; // IE10+ + user-select: @select; +} diff --git a/frontend/css/vendors/less/modals.less b/frontend/css/vendors/less/modals.less new file mode 100644 index 0000000..767ce36 --- /dev/null +++ b/frontend/css/vendors/less/modals.less @@ -0,0 +1,150 @@ +// +// Modals +// -------------------------------------------------- + +// .modal-open - body class for killing the scroll +// .modal - container to scroll within +// .modal-dialog - positioning shell for the actual modal +// .modal-content - actual modal w/ bg and corners and shit + +// Kill the scroll on the body +.modal-open { + overflow: hidden; +} + +// Container that the modal scrolls within +.modal { + display: none; + overflow: hidden; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: @zindex-modal; + -webkit-overflow-scrolling: touch; + + // Prevent Chrome on Windows from adding a focus outline. For details, see + // https://github.com/twbs/bootstrap/pull/10951. + outline: 0; + + // When fading in the modal, animate it to slide down + &.fade .modal-dialog { + .translate(0, -25%); + .transition-transform(~"0.3s ease-out"); + } + &.in .modal-dialog { .translate(0, 0) } +} +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} + +// Shell div to position the modal with bottom padding +.modal-dialog { + position: relative; + width: auto; + margin: 10px; +} + +// Actual modal +.modal-content { + position: relative; + background-color: @modal-content-bg; + border: 1px solid @modal-content-fallback-border-color; //old browsers fallback (ie8 etc) + border: 1px solid @modal-content-border-color; + border-radius: @border-radius-large; + .box-shadow(0 3px 9px rgba(0,0,0,.5)); + background-clip: padding-box; + // Remove focus outline from opened modal + outline: 0; +} + +// Modal background +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: @zindex-modal-background; + background-color: @modal-backdrop-bg; + // Fade for backdrop + &.fade { .opacity(0); } + &.in { .opacity(@modal-backdrop-opacity); } +} + +// Modal header +// Top section of the modal w/ title and dismiss +.modal-header { + padding: @modal-title-padding; + border-bottom: 1px solid @modal-header-border-color; + &:extend(.clearfix all); +} +// Close icon +.modal-header .close { + margin-top: -2px; +} + +// Title text within header +.modal-title { + margin: 0; + line-height: @modal-title-line-height; +} + +// Modal body +// Where all modal content resides (sibling of .modal-header and .modal-footer) +.modal-body { + position: relative; + padding: @modal-inner-padding; +} + +// Footer (for actions) +.modal-footer { + padding: @modal-inner-padding; + text-align: right; // right align buttons + border-top: 1px solid @modal-footer-border-color; + &:extend(.clearfix all); // clear it in case folks use .pull-* classes on buttons + + // Properly space out buttons + .btn + .btn { + margin-left: 5px; + margin-bottom: 0; // account for input[type="submit"] which gets the bottom margin like all other inputs + } + // but override that for button groups + .btn-group .btn + .btn { + margin-left: -1px; + } + // and override it for block buttons as well + .btn-block + .btn-block { + margin-left: 0; + } +} + +// Measure scrollbar width for padding body during modal show/hide +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} + +// Scale up the modal +@media (min-width: @screen-sm-min) { + // Automatically set modal's width for larger viewports + .modal-dialog { + width: @modal-md; + margin: 30px auto; + } + .modal-content { + .box-shadow(0 5px 15px rgba(0,0,0,.5)); + } + + // Modal sizes + .modal-sm { width: @modal-sm; } +} + +@media (min-width: @screen-md-min) { + .modal-lg { width: @modal-lg; } +} diff --git a/frontend/css/vendors/less/navbar.less b/frontend/css/vendors/less/navbar.less new file mode 100644 index 0000000..6d751bb --- /dev/null +++ b/frontend/css/vendors/less/navbar.less @@ -0,0 +1,660 @@ +// +// Navbars +// -------------------------------------------------- + + +// Wrapper and base class +// +// Provide a static navbar from which we expand to create full-width, fixed, and +// other navbar variations. + +.navbar { + position: relative; + min-height: @navbar-height; // Ensure a navbar always shows (e.g., without a .navbar-brand in collapsed mode) + margin-bottom: @navbar-margin-bottom; + border: 1px solid transparent; + + // Prevent floats from breaking the navbar + &:extend(.clearfix all); + + @media (min-width: @grid-float-breakpoint) { + border-radius: @navbar-border-radius; + } +} + + +// Navbar heading +// +// Groups `.navbar-brand` and `.navbar-toggle` into a single component for easy +// styling of responsive aspects. + +.navbar-header { + &:extend(.clearfix all); + + @media (min-width: @grid-float-breakpoint) { + float: left; + } +} + + +// Navbar collapse (body) +// +// Group your navbar content into this for easy collapsing and expanding across +// various device sizes. By default, this content is collapsed when <768px, but +// will expand past that for a horizontal display. +// +// To start (on mobile devices) the navbar links, forms, and buttons are stacked +// vertically and include a `max-height` to overflow in case you have too much +// content for the user's viewport. + +.navbar-collapse { + overflow-x: visible; + padding-right: @navbar-padding-horizontal; + padding-left: @navbar-padding-horizontal; + border-top: 1px solid transparent; + box-shadow: inset 0 1px 0 rgba(255,255,255,.1); + &:extend(.clearfix all); + -webkit-overflow-scrolling: touch; + + &.in { + overflow-y: auto; + } + + @media (min-width: @grid-float-breakpoint) { + width: auto; + border-top: 0; + box-shadow: none; + + &.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; // Override default setting + overflow: visible !important; + } + + &.in { + overflow-y: visible; + } + + // Undo the collapse side padding for navbars with containers to ensure + // alignment of right-aligned contents. + .navbar-fixed-top &, + .navbar-static-top &, + .navbar-fixed-bottom & { + padding-left: 0; + padding-right: 0; + } + } +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + .navbar-collapse { + max-height: @navbar-collapse-max-height; + + @media (max-device-width: @screen-xs-min) and (orientation: landscape) { + max-height: 200px; + } + } +} + + +// Both navbar header and collapse +// +// When a container is present, change the behavior of the header and collapse. + +.container, +.container-fluid { + > .navbar-header, + > .navbar-collapse { + margin-right: -@navbar-padding-horizontal; + margin-left: -@navbar-padding-horizontal; + + @media (min-width: @grid-float-breakpoint) { + margin-right: 0; + margin-left: 0; + } + } +} + + +// +// Navbar alignment options +// +// Display the navbar across the entirety of the page or fixed it to the top or +// bottom of the page. + +// Static top (unfixed, but 100% wide) navbar +.navbar-static-top { + z-index: @zindex-navbar; + border-width: 0 0 1px; + + @media (min-width: @grid-float-breakpoint) { + border-radius: 0; + } +} + +// Fix the top/bottom navbars when screen real estate supports it +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: @zindex-navbar-fixed; + + // Undo the rounded corners + @media (min-width: @grid-float-breakpoint) { + border-radius: 0; + } +} +.navbar-fixed-top { + top: 0; + border-width: 0 0 1px; +} +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; // override .navbar defaults + border-width: 1px 0 0; +} + + +// Brand/project name + +.navbar-brand { + float: left; + padding: @navbar-padding-vertical @navbar-padding-horizontal; + font-size: @font-size-large; + line-height: @line-height-computed; + height: @navbar-height; + + &:hover, + &:focus { + text-decoration: none; + } + + > img { + display: block; + } + + @media (min-width: @grid-float-breakpoint) { + .navbar > .container &, + .navbar > .container-fluid & { + margin-left: -@navbar-padding-horizontal; + } + } +} + + +// Navbar toggle +// +// Custom button for toggling the `.navbar-collapse`, powered by the collapse +// JavaScript plugin. + +.navbar-toggle { + position: relative; + float: right; + margin-right: @navbar-padding-horizontal; + padding: 9px 10px; + .navbar-vertical-align(34px); + background-color: transparent; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid transparent; + border-radius: @border-radius-base; + + // We remove the `outline` here, but later compensate by attaching `:hover` + // styles to `:focus`. + &:focus { + outline: 0; + } + + // Bars + .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; + } + .icon-bar + .icon-bar { + margin-top: 4px; + } + + @media (min-width: @grid-float-breakpoint) { + display: none; + } +} + + +// Navbar nav links +// +// Builds on top of the `.nav` components with its own modifier class to make +// the nav the full height of the horizontal nav (above 768px). + +.navbar-nav { + margin: (@navbar-padding-vertical / 2) -@navbar-padding-horizontal; + + > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: @line-height-computed; + } + + @media (max-width: @grid-float-breakpoint-max) { + // Dropdowns get custom display when collapsed + .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + box-shadow: none; + > li > a, + .dropdown-header { + padding: 5px 15px 5px 25px; + } + > li > a { + line-height: @line-height-computed; + &:hover, + &:focus { + background-image: none; + } + } + } + } + + // Uncollapse the nav + @media (min-width: @grid-float-breakpoint) { + float: left; + margin: 0; + + > li { + float: left; + > a { + padding-top: @navbar-padding-vertical; + padding-bottom: @navbar-padding-vertical; + } + } + } +} + + +// Navbar form +// +// Extension of the `.form-inline` with some extra flavor for optimum display in +// our navbars. + +.navbar-form { + margin-left: -@navbar-padding-horizontal; + margin-right: -@navbar-padding-horizontal; + padding: 10px @navbar-padding-horizontal; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + @shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1); + .box-shadow(@shadow); + + // Mixin behavior for optimum display + .form-inline(); + + .form-group { + @media (max-width: @grid-float-breakpoint-max) { + margin-bottom: 5px; + + &:last-child { + margin-bottom: 0; + } + } + } + + // Vertically center in expanded, horizontal navbar + .navbar-vertical-align(@input-height-base); + + // Undo 100% width for pull classes + @media (min-width: @grid-float-breakpoint) { + width: auto; + border: 0; + margin-left: 0; + margin-right: 0; + padding-top: 0; + padding-bottom: 0; + .box-shadow(none); + } +} + + +// Dropdown menus + +// Menu position and menu carets +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + .border-top-radius(0); +} +// Menu position and menu caret support for dropups via extra dropup class +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + margin-bottom: 0; + .border-top-radius(@navbar-border-radius); + .border-bottom-radius(0); +} + + +// Buttons in navbars +// +// Vertically center a button within a navbar (when *not* in a form). + +.navbar-btn { + .navbar-vertical-align(@input-height-base); + + &.btn-sm { + .navbar-vertical-align(@input-height-small); + } + &.btn-xs { + .navbar-vertical-align(22); + } +} + + +// Text in navbars +// +// Add a class to make any element properly align itself vertically within the navbars. + +.navbar-text { + .navbar-vertical-align(@line-height-computed); + + @media (min-width: @grid-float-breakpoint) { + float: left; + margin-left: @navbar-padding-horizontal; + margin-right: @navbar-padding-horizontal; + } +} + + +// Component alignment +// +// Repurpose the pull utilities as their own navbar utilities to avoid specificity +// issues with parents and chaining. Only do this when the navbar is uncollapsed +// though so that navbar contents properly stack and align in mobile. +// +// Declared after the navbar components to ensure more specificity on the margins. + +@media (min-width: @grid-float-breakpoint) { + .navbar-left { .pull-left(); } + .navbar-right { + .pull-right(); + margin-right: -@navbar-padding-horizontal; + + ~ .navbar-right { + margin-right: 0; + } + } +} + + +// Alternate navbars +// -------------------------------------------------- + +// Default navbar +.navbar-default { + background-color: @navbar-default-bg; + border-color: @navbar-default-border; + + .navbar-brand { + color: @navbar-default-brand-color; + &:hover, + &:focus { + color: @navbar-default-brand-hover-color; + background-color: @navbar-default-brand-hover-bg; + } + } + + .navbar-text { + color: @navbar-default-color; + } + + .navbar-nav { + > li > a { + color: @navbar-default-link-color; + + &:hover, + &:focus { + color: @navbar-default-link-hover-color; + background-color: @navbar-default-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: @navbar-default-link-active-color; + background-color: @navbar-default-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: @navbar-default-link-disabled-color; + background-color: @navbar-default-link-disabled-bg; + } + } + } + + .navbar-toggle { + border-color: @navbar-default-toggle-border-color; + &:hover, + &:focus { + background-color: @navbar-default-toggle-hover-bg; + } + .icon-bar { + background-color: @navbar-default-toggle-icon-bar-bg; + } + } + + .navbar-collapse, + .navbar-form { + border-color: @navbar-default-border; + } + + // Dropdown menu items + .navbar-nav { + // Remove background color from open dropdown + > .open > a { + &, + &:hover, + &:focus { + background-color: @navbar-default-link-active-bg; + color: @navbar-default-link-active-color; + } + } + + @media (max-width: @grid-float-breakpoint-max) { + // Dropdowns get custom display when collapsed + .open .dropdown-menu { + > li > a { + color: @navbar-default-link-color; + &:hover, + &:focus { + color: @navbar-default-link-hover-color; + background-color: @navbar-default-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: @navbar-default-link-active-color; + background-color: @navbar-default-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: @navbar-default-link-disabled-color; + background-color: @navbar-default-link-disabled-bg; + } + } + } + } + } + + + // Links in navbars + // + // Add a class to ensure links outside the navbar nav are colored correctly. + + .navbar-link { + color: @navbar-default-link-color; + &:hover { + color: @navbar-default-link-hover-color; + } + } + + .btn-link { + color: @navbar-default-link-color; + &:hover, + &:focus { + color: @navbar-default-link-hover-color; + } + &[disabled], + fieldset[disabled] & { + &:hover, + &:focus { + color: @navbar-default-link-disabled-color; + } + } + } +} + +// Inverse navbar + +.navbar-inverse { + background-color: @navbar-inverse-bg; + border-color: @navbar-inverse-border; + + .navbar-brand { + color: @navbar-inverse-brand-color; + &:hover, + &:focus { + color: @navbar-inverse-brand-hover-color; + background-color: @navbar-inverse-brand-hover-bg; + } + } + + .navbar-text { + color: @navbar-inverse-color; + } + + .navbar-nav { + > li > a { + color: @navbar-inverse-link-color; + + &:hover, + &:focus { + color: @navbar-inverse-link-hover-color; + background-color: @navbar-inverse-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: @navbar-inverse-link-active-color; + background-color: @navbar-inverse-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: @navbar-inverse-link-disabled-color; + background-color: @navbar-inverse-link-disabled-bg; + } + } + } + + // Darken the responsive nav toggle + .navbar-toggle { + border-color: @navbar-inverse-toggle-border-color; + &:hover, + &:focus { + background-color: @navbar-inverse-toggle-hover-bg; + } + .icon-bar { + background-color: @navbar-inverse-toggle-icon-bar-bg; + } + } + + .navbar-collapse, + .navbar-form { + border-color: darken(@navbar-inverse-bg, 7%); + } + + // Dropdowns + .navbar-nav { + > .open > a { + &, + &:hover, + &:focus { + background-color: @navbar-inverse-link-active-bg; + color: @navbar-inverse-link-active-color; + } + } + + @media (max-width: @grid-float-breakpoint-max) { + // Dropdowns get custom display + .open .dropdown-menu { + > .dropdown-header { + border-color: @navbar-inverse-border; + } + .divider { + background-color: @navbar-inverse-border; + } + > li > a { + color: @navbar-inverse-link-color; + &:hover, + &:focus { + color: @navbar-inverse-link-hover-color; + background-color: @navbar-inverse-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: @navbar-inverse-link-active-color; + background-color: @navbar-inverse-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: @navbar-inverse-link-disabled-color; + background-color: @navbar-inverse-link-disabled-bg; + } + } + } + } + } + + .navbar-link { + color: @navbar-inverse-link-color; + &:hover { + color: @navbar-inverse-link-hover-color; + } + } + + .btn-link { + color: @navbar-inverse-link-color; + &:hover, + &:focus { + color: @navbar-inverse-link-hover-color; + } + &[disabled], + fieldset[disabled] & { + &:hover, + &:focus { + color: @navbar-inverse-link-disabled-color; + } + } + } +} diff --git a/frontend/css/vendors/less/navs.less b/frontend/css/vendors/less/navs.less new file mode 100644 index 0000000..a3d11b1 --- /dev/null +++ b/frontend/css/vendors/less/navs.less @@ -0,0 +1,242 @@ +// +// Navs +// -------------------------------------------------- + + +// Base class +// -------------------------------------------------- + +.nav { + margin-bottom: 0; + padding-left: 0; // Override default ul/ol + list-style: none; + &:extend(.clearfix all); + + > li { + position: relative; + display: block; + + > a { + position: relative; + display: block; + padding: @nav-link-padding; + &:hover, + &:focus { + text-decoration: none; + background-color: @nav-link-hover-bg; + } + } + + // Disabled state sets text to gray and nukes hover/tab effects + &.disabled > a { + color: @nav-disabled-link-color; + + &:hover, + &:focus { + color: @nav-disabled-link-hover-color; + text-decoration: none; + background-color: transparent; + cursor: @cursor-disabled; + } + } + } + + // Open dropdowns + .open > a { + &, + &:hover, + &:focus { + background-color: @nav-link-hover-bg; + border-color: @link-color; + } + } + + // Nav dividers (deprecated with v3.0.1) + // + // This should have been removed in v3 with the dropping of `.nav-list`, but + // we missed it. We don't currently support this anywhere, but in the interest + // of maintaining backward compatibility in case you use it, it's deprecated. + .nav-divider { + .nav-divider(); + } + + // Prevent IE8 from misplacing imgs + // + // See https://github.com/h5bp/html5-boilerplate/issues/984#issuecomment-3985989 + > li > a > img { + max-width: none; + } +} + + +// Tabs +// ------------------------- + +// Give the tabs something to sit on +.nav-tabs { + border-bottom: 1px solid @nav-tabs-border-color; + > li { + float: left; + // Make the list-items overlay the bottom border + margin-bottom: -1px; + + // Actual tabs (as links) + > a { + margin-right: 2px; + line-height: @line-height-base; + border: 1px solid transparent; + border-radius: @border-radius-base @border-radius-base 0 0; + &:hover { + border-color: @nav-tabs-link-hover-border-color @nav-tabs-link-hover-border-color @nav-tabs-border-color; + } + } + + // Active state, and its :hover to override normal :hover + &.active > a { + &, + &:hover, + &:focus { + color: @nav-tabs-active-link-hover-color; + background-color: @nav-tabs-active-link-hover-bg; + border: 1px solid @nav-tabs-active-link-hover-border-color; + border-bottom-color: transparent; + cursor: default; + } + } + } + // pulling this in mainly for less shorthand + &.nav-justified { + .nav-justified(); + .nav-tabs-justified(); + } +} + + +// Pills +// ------------------------- +.nav-pills { + > li { + float: left; + + // Links rendered as pills + > a { + border-radius: @nav-pills-border-radius; + } + + li { + margin-left: 2px; + } + + // Active state + &.active > a { + &, + &:hover, + &:focus { + color: @nav-pills-active-link-hover-color; + background-color: @nav-pills-active-link-hover-bg; + } + } + } +} + + +// Stacked pills +.nav-stacked { + > li { + float: none; + + li { + margin-top: 2px; + margin-left: 0; // no need for this gap between nav items + } + } +} + + +// Nav variations +// -------------------------------------------------- + +// Justified nav links +// ------------------------- + +.nav-justified { + width: 100%; + + > li { + float: none; + > a { + text-align: center; + margin-bottom: 5px; + } + } + + > .dropdown .dropdown-menu { + top: auto; + left: auto; + } + + @media (min-width: @screen-sm-min) { + > li { + display: table-cell; + width: 1%; + > a { + margin-bottom: 0; + } + } + } +} + +// Move borders to anchors instead of bottom of list +// +// Mixin for adding on top the shared `.nav-justified` styles for our tabs +.nav-tabs-justified { + border-bottom: 0; + + > li > a { + // Override margin from .nav-tabs + margin-right: 0; + border-radius: @border-radius-base; + } + + > .active > a, + > .active > a:hover, + > .active > a:focus { + border: 1px solid @nav-tabs-justified-link-border-color; + } + + @media (min-width: @screen-sm-min) { + > li > a { + border-bottom: 1px solid @nav-tabs-justified-link-border-color; + border-radius: @border-radius-base @border-radius-base 0 0; + } + > .active > a, + > .active > a:hover, + > .active > a:focus { + border-bottom-color: @nav-tabs-justified-active-link-border-color; + } + } +} + + +// Tabbable tabs +// ------------------------- + +// Hide tabbable panes to start, show them when `.active` +.tab-content { + > .tab-pane { + display: none; + } + > .active { + display: block; + } +} + + +// Dropdowns +// ------------------------- + +// Specific dropdowns +.nav-tabs .dropdown-menu { + // make dropdown border overlap tab border + margin-top: -1px; + // Remove the top rounded corners here since there is a hard edge above the menu + .border-top-radius(0); +} diff --git a/frontend/css/vendors/less/normalize.less b/frontend/css/vendors/less/normalize.less new file mode 100644 index 0000000..9dddf73 --- /dev/null +++ b/frontend/css/vendors/less/normalize.less @@ -0,0 +1,424 @@ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ + +// +// 1. Set default font family to sans-serif. +// 2. Prevent iOS and IE text size adjust after device orientation change, +// without disabling user zoom. +// + +html { + font-family: sans-serif; // 1 + -ms-text-size-adjust: 100%; // 2 + -webkit-text-size-adjust: 100%; // 2 +} + +// +// Remove default margin. +// + +body { + margin: 0; +} + +// HTML5 display definitions +// ========================================================================== + +// +// Correct `block` display not defined for any HTML5 element in IE 8/9. +// Correct `block` display not defined for `details` or `summary` in IE 10/11 +// and Firefox. +// Correct `block` display not defined for `main` in IE 11. +// + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} + +// +// 1. Correct `inline-block` display not defined in IE 8/9. +// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. +// + +audio, +canvas, +progress, +video { + display: inline-block; // 1 + vertical-align: baseline; // 2 +} + +// +// Prevent modern browsers from displaying `audio` without controls. +// Remove excess height in iOS 5 devices. +// + +audio:not([controls]) { + display: none; + height: 0; +} + +// +// Address `[hidden]` styling not present in IE 8/9/10. +// Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. +// + +[hidden], +template { + display: none; +} + +// Links +// ========================================================================== + +// +// Remove the gray background color from active links in IE 10. +// + +a { + background-color: transparent; +} + +// +// Improve readability of focused elements when they are also in an +// active/hover state. +// + +a:active, +a:hover { + outline: 0; +} + +// Text-level semantics +// ========================================================================== + +// +// Address styling not present in IE 8/9/10/11, Safari, and Chrome. +// + +abbr[title] { + border-bottom: 1px dotted; +} + +// +// Address style set to `bolder` in Firefox 4+, Safari, and Chrome. +// + +b, +strong { + font-weight: bold; +} + +// +// Address styling not present in Safari and Chrome. +// + +dfn { + font-style: italic; +} + +// +// Address variable `h1` font-size and margin within `section` and `article` +// contexts in Firefox 4+, Safari, and Chrome. +// + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +// +// Address styling not present in IE 8/9. +// + +mark { + background: #ff0; + color: #000; +} + +// +// Address inconsistent and variable font size in all browsers. +// + +small { + font-size: 80%; +} + +// +// Prevent `sub` and `sup` affecting `line-height` in all browsers. +// + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +// Embedded content +// ========================================================================== + +// +// Remove border when inside `a` element in IE 8/9/10. +// + +img { + border: 0; +} + +// +// Correct overflow not hidden in IE 9/10/11. +// + +svg:not(:root) { + overflow: hidden; +} + +// Grouping content +// ========================================================================== + +// +// Address margin not present in IE 8/9 and Safari. +// + +figure { + margin: 1em 40px; +} + +// +// Address differences between Firefox and other browsers. +// + +hr { + box-sizing: content-box; + height: 0; +} + +// +// Contain overflow in all browsers. +// + +pre { + overflow: auto; +} + +// +// Address odd `em`-unit font size rendering in all browsers. +// + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +// Forms +// ========================================================================== + +// +// Known limitation: by default, Chrome and Safari on OS X allow very limited +// styling of `select`, unless a `border` property is set. +// + +// +// 1. Correct color not being inherited. +// Known issue: affects color of disabled elements. +// 2. Correct font properties not being inherited. +// 3. Address margins set differently in Firefox 4+, Safari, and Chrome. +// + +button, +input, +optgroup, +select, +textarea { + color: inherit; // 1 + font: inherit; // 2 + margin: 0; // 3 +} + +// +// Address `overflow` set to `hidden` in IE 8/9/10/11. +// + +button { + overflow: visible; +} + +// +// Address inconsistent `text-transform` inheritance for `button` and `select`. +// All other form control elements do not inherit `text-transform` values. +// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. +// Correct `select` style inheritance in Firefox. +// + +button, +select { + text-transform: none; +} + +// +// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` +// and `video` controls. +// 2. Correct inability to style clickable `input` types in iOS. +// 3. Improve usability and consistency of cursor style between image-type +// `input` and others. +// + +button, +html input[type="button"], // 1 +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; // 2 + cursor: pointer; // 3 +} + +// +// Re-set default cursor for disabled elements. +// + +button[disabled], +html input[disabled] { + cursor: default; +} + +// +// Remove inner padding and border in Firefox 4+. +// + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +// +// Address Firefox 4+ setting `line-height` on `input` using `!important` in +// the UA stylesheet. +// + +input { + line-height: normal; +} + +// +// It's recommended that you don't attempt to style these elements. +// Firefox's implementation doesn't respect box-sizing, padding, or width. +// +// 1. Address box sizing set to `content-box` in IE 8/9/10. +// 2. Remove excess padding in IE 8/9/10. +// + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; // 1 + padding: 0; // 2 +} + +// +// Fix the cursor style for Chrome's increment/decrement buttons. For certain +// `font-size` values of the `input`, it causes the cursor style of the +// decrement button to change from `default` to `text`. +// + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +// +// 1. Address `appearance` set to `searchfield` in Safari and Chrome. +// 2. Address `box-sizing` set to `border-box` in Safari and Chrome. +// + +input[type="search"] { + -webkit-appearance: textfield; // 1 + box-sizing: content-box; //2 +} + +// +// Remove inner padding and search cancel button in Safari and Chrome on OS X. +// Safari (but not Chrome) clips the cancel button when the search input has +// padding (and `textfield` appearance). +// + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +// +// Define consistent border, margin, and padding. +// + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +// +// 1. Correct `color` not being inherited in IE 8/9/10/11. +// 2. Remove padding so people aren't caught out if they zero out fieldsets. +// + +legend { + border: 0; // 1 + padding: 0; // 2 +} + +// +// Remove default vertical scrollbar in IE 8/9/10/11. +// + +textarea { + overflow: auto; +} + +// +// Don't inherit the `font-weight` (applied by a rule above). +// NOTE: the default cannot safely be changed in Chrome and Safari on OS X. +// + +optgroup { + font-weight: bold; +} + +// Tables +// ========================================================================== + +// +// Remove most spacing between table cells. +// + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} diff --git a/frontend/css/vendors/less/pager.less b/frontend/css/vendors/less/pager.less new file mode 100644 index 0000000..41abaaa --- /dev/null +++ b/frontend/css/vendors/less/pager.less @@ -0,0 +1,54 @@ +// +// Pager pagination +// -------------------------------------------------- + + +.pager { + padding-left: 0; + margin: @line-height-computed 0; + list-style: none; + text-align: center; + &:extend(.clearfix all); + li { + display: inline; + > a, + > span { + display: inline-block; + padding: 5px 14px; + background-color: @pager-bg; + border: 1px solid @pager-border; + border-radius: @pager-border-radius; + } + + > a:hover, + > a:focus { + text-decoration: none; + background-color: @pager-hover-bg; + } + } + + .next { + > a, + > span { + float: right; + } + } + + .previous { + > a, + > span { + float: left; + } + } + + .disabled { + > a, + > a:hover, + > a:focus, + > span { + color: @pager-disabled-color; + background-color: @pager-bg; + cursor: @cursor-disabled; + } + } +} diff --git a/frontend/css/vendors/less/pagination.less b/frontend/css/vendors/less/pagination.less new file mode 100644 index 0000000..31f77aa --- /dev/null +++ b/frontend/css/vendors/less/pagination.less @@ -0,0 +1,89 @@ +// +// Pagination (multiple pages) +// -------------------------------------------------- +.pagination { + display: inline-block; + padding-left: 0; + margin: @line-height-computed 0; + border-radius: @border-radius-base; + + > li { + display: inline; // Remove list-style and block-level defaults + > a, + > span { + position: relative; + float: left; // Collapse white-space + padding: @padding-base-vertical @padding-base-horizontal; + line-height: @line-height-base; + text-decoration: none; + color: @pagination-color; + background-color: @pagination-bg; + border: 1px solid @pagination-border; + margin-left: -1px; + } + &:first-child { + > a, + > span { + margin-left: 0; + .border-left-radius(@border-radius-base); + } + } + &:last-child { + > a, + > span { + .border-right-radius(@border-radius-base); + } + } + } + + > li > a, + > li > span { + &:hover, + &:focus { + z-index: 2; + color: @pagination-hover-color; + background-color: @pagination-hover-bg; + border-color: @pagination-hover-border; + } + } + + > .active > a, + > .active > span { + &, + &:hover, + &:focus { + z-index: 3; + color: @pagination-active-color; + background-color: @pagination-active-bg; + border-color: @pagination-active-border; + cursor: default; + } + } + + > .disabled { + > span, + > span:hover, + > span:focus, + > a, + > a:hover, + > a:focus { + color: @pagination-disabled-color; + background-color: @pagination-disabled-bg; + border-color: @pagination-disabled-border; + cursor: @cursor-disabled; + } + } +} + +// Sizing +// -------------------------------------------------- + +// Large +.pagination-lg { + .pagination-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large); +} + +// Small +.pagination-sm { + .pagination-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small); +} diff --git a/frontend/css/vendors/less/panels.less b/frontend/css/vendors/less/panels.less new file mode 100644 index 0000000..425eb5e --- /dev/null +++ b/frontend/css/vendors/less/panels.less @@ -0,0 +1,271 @@ +// +// Panels +// -------------------------------------------------- + + +// Base class +.panel { + margin-bottom: @line-height-computed; + background-color: @panel-bg; + border: 1px solid transparent; + border-radius: @panel-border-radius; + .box-shadow(0 1px 1px rgba(0,0,0,.05)); +} + +// Panel contents +.panel-body { + padding: @panel-body-padding; + &:extend(.clearfix all); +} + +// Optional heading +.panel-heading { + padding: @panel-heading-padding; + border-bottom: 1px solid transparent; + .border-top-radius((@panel-border-radius - 1)); + + > .dropdown .dropdown-toggle { + color: inherit; + } +} + +// Within heading, strip any `h*` tag of its default margins for spacing. +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: ceil((@font-size-base * 1.125)); + color: inherit; + + > a, + > small, + > .small, + > small > a, + > .small > a { + color: inherit; + } +} + +// Optional footer (stays gray in every modifier class) +.panel-footer { + padding: @panel-footer-padding; + background-color: @panel-footer-bg; + border-top: 1px solid @panel-inner-border; + .border-bottom-radius((@panel-border-radius - 1)); +} + + +// List groups in panels +// +// By default, space out list group content from panel headings to account for +// any kind of custom content between the two. + +.panel { + > .list-group, + > .panel-collapse > .list-group { + margin-bottom: 0; + + .list-group-item { + border-width: 1px 0; + border-radius: 0; + } + + // Add border top radius for first one + &:first-child { + .list-group-item:first-child { + border-top: 0; + .border-top-radius((@panel-border-radius - 1)); + } + } + + // Add border bottom radius for last one + &:last-child { + .list-group-item:last-child { + border-bottom: 0; + .border-bottom-radius((@panel-border-radius - 1)); + } + } + } + > .panel-heading + .panel-collapse > .list-group { + .list-group-item:first-child { + .border-top-radius(0); + } + } +} +// Collapse space between when there's no additional content. +.panel-heading + .list-group { + .list-group-item:first-child { + border-top-width: 0; + } +} +.list-group + .panel-footer { + border-top-width: 0; +} + +// Tables in panels +// +// Place a non-bordered `.table` within a panel (not within a `.panel-body`) and +// watch it go full width. + +.panel { + > .table, + > .table-responsive > .table, + > .panel-collapse > .table { + margin-bottom: 0; + + caption { + padding-left: @panel-body-padding; + padding-right: @panel-body-padding; + } + } + // Add border top radius for first one + > .table:first-child, + > .table-responsive:first-child > .table:first-child { + .border-top-radius((@panel-border-radius - 1)); + + > thead:first-child, + > tbody:first-child { + > tr:first-child { + border-top-left-radius: (@panel-border-radius - 1); + border-top-right-radius: (@panel-border-radius - 1); + + td:first-child, + th:first-child { + border-top-left-radius: (@panel-border-radius - 1); + } + td:last-child, + th:last-child { + border-top-right-radius: (@panel-border-radius - 1); + } + } + } + } + // Add border bottom radius for last one + > .table:last-child, + > .table-responsive:last-child > .table:last-child { + .border-bottom-radius((@panel-border-radius - 1)); + + > tbody:last-child, + > tfoot:last-child { + > tr:last-child { + border-bottom-left-radius: (@panel-border-radius - 1); + border-bottom-right-radius: (@panel-border-radius - 1); + + td:first-child, + th:first-child { + border-bottom-left-radius: (@panel-border-radius - 1); + } + td:last-child, + th:last-child { + border-bottom-right-radius: (@panel-border-radius - 1); + } + } + } + } + > .panel-body + .table, + > .panel-body + .table-responsive, + > .table + .panel-body, + > .table-responsive + .panel-body { + border-top: 1px solid @table-border-color; + } + > .table > tbody:first-child > tr:first-child th, + > .table > tbody:first-child > tr:first-child td { + border-top: 0; + } + > .table-bordered, + > .table-responsive > .table-bordered { + border: 0; + > thead, + > tbody, + > tfoot { + > tr { + > th:first-child, + > td:first-child { + border-left: 0; + } + > th:last-child, + > td:last-child { + border-right: 0; + } + } + } + > thead, + > tbody { + > tr:first-child { + > td, + > th { + border-bottom: 0; + } + } + } + > tbody, + > tfoot { + > tr:last-child { + > td, + > th { + border-bottom: 0; + } + } + } + } + > .table-responsive { + border: 0; + margin-bottom: 0; + } +} + + +// Collapsable panels (aka, accordion) +// +// Wrap a series of panels in `.panel-group` to turn them into an accordion with +// the help of our collapse JavaScript plugin. + +.panel-group { + margin-bottom: @line-height-computed; + + // Tighten up margin so it's only between panels + .panel { + margin-bottom: 0; + border-radius: @panel-border-radius; + + + .panel { + margin-top: 5px; + } + } + + .panel-heading { + border-bottom: 0; + + + .panel-collapse > .panel-body, + + .panel-collapse > .list-group { + border-top: 1px solid @panel-inner-border; + } + } + + .panel-footer { + border-top: 0; + + .panel-collapse .panel-body { + border-bottom: 1px solid @panel-inner-border; + } + } +} + + +// Contextual variations +.panel-default { + .panel-variant(@panel-default-border; @panel-default-text; @panel-default-heading-bg; @panel-default-border); +} +.panel-primary { + .panel-variant(@panel-primary-border; @panel-primary-text; @panel-primary-heading-bg; @panel-primary-border); +} +.panel-success { + .panel-variant(@panel-success-border; @panel-success-text; @panel-success-heading-bg; @panel-success-border); +} +.panel-info { + .panel-variant(@panel-info-border; @panel-info-text; @panel-info-heading-bg; @panel-info-border); +} +.panel-warning { + .panel-variant(@panel-warning-border; @panel-warning-text; @panel-warning-heading-bg; @panel-warning-border); +} +.panel-danger { + .panel-variant(@panel-danger-border; @panel-danger-text; @panel-danger-heading-bg; @panel-danger-border); +} diff --git a/frontend/css/vendors/less/popovers.less b/frontend/css/vendors/less/popovers.less new file mode 100644 index 0000000..3a62a64 --- /dev/null +++ b/frontend/css/vendors/less/popovers.less @@ -0,0 +1,131 @@ +// +// Popovers +// -------------------------------------------------- + + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: @zindex-popover; + display: none; + max-width: @popover-max-width; + padding: 1px; + // Our parent element can be arbitrary since popovers are by default inserted as a sibling of their target element. + // So reset our font and text properties to avoid inheriting weird values. + .reset-text(); + font-size: @font-size-base; + + background-color: @popover-bg; + background-clip: padding-box; + border: 1px solid @popover-fallback-border-color; + border: 1px solid @popover-border-color; + border-radius: @border-radius-large; + .box-shadow(0 5px 10px rgba(0,0,0,.2)); + + // Offset the popover to account for the popover arrow + &.top { margin-top: -@popover-arrow-width; } + &.right { margin-left: @popover-arrow-width; } + &.bottom { margin-top: @popover-arrow-width; } + &.left { margin-left: -@popover-arrow-width; } +} + +.popover-title { + margin: 0; // reset heading margin + padding: 8px 14px; + font-size: @font-size-base; + background-color: @popover-title-bg; + border-bottom: 1px solid darken(@popover-title-bg, 5%); + border-radius: (@border-radius-large - 1) (@border-radius-large - 1) 0 0; +} + +.popover-content { + padding: 9px 14px; +} + +// Arrows +// +// .arrow is outer, .arrow:after is inner + +.popover > .arrow { + &, + &:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + } +} +.popover > .arrow { + border-width: @popover-arrow-outer-width; +} +.popover > .arrow:after { + border-width: @popover-arrow-width; + content: ""; +} + +.popover { + &.top > .arrow { + left: 50%; + margin-left: -@popover-arrow-outer-width; + border-bottom-width: 0; + border-top-color: @popover-arrow-outer-fallback-color; // IE8 fallback + border-top-color: @popover-arrow-outer-color; + bottom: -@popover-arrow-outer-width; + &:after { + content: " "; + bottom: 1px; + margin-left: -@popover-arrow-width; + border-bottom-width: 0; + border-top-color: @popover-arrow-color; + } + } + &.right > .arrow { + top: 50%; + left: -@popover-arrow-outer-width; + margin-top: -@popover-arrow-outer-width; + border-left-width: 0; + border-right-color: @popover-arrow-outer-fallback-color; // IE8 fallback + border-right-color: @popover-arrow-outer-color; + &:after { + content: " "; + left: 1px; + bottom: -@popover-arrow-width; + border-left-width: 0; + border-right-color: @popover-arrow-color; + } + } + &.bottom > .arrow { + left: 50%; + margin-left: -@popover-arrow-outer-width; + border-top-width: 0; + border-bottom-color: @popover-arrow-outer-fallback-color; // IE8 fallback + border-bottom-color: @popover-arrow-outer-color; + top: -@popover-arrow-outer-width; + &:after { + content: " "; + top: 1px; + margin-left: -@popover-arrow-width; + border-top-width: 0; + border-bottom-color: @popover-arrow-color; + } + } + + &.left > .arrow { + top: 50%; + right: -@popover-arrow-outer-width; + margin-top: -@popover-arrow-outer-width; + border-right-width: 0; + border-left-color: @popover-arrow-outer-fallback-color; // IE8 fallback + border-left-color: @popover-arrow-outer-color; + &:after { + content: " "; + right: 1px; + border-right-width: 0; + border-left-color: @popover-arrow-color; + bottom: -@popover-arrow-width; + } + } +} diff --git a/frontend/css/vendors/less/print.less b/frontend/css/vendors/less/print.less new file mode 100644 index 0000000..66e54ab --- /dev/null +++ b/frontend/css/vendors/less/print.less @@ -0,0 +1,101 @@ +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ + +// ========================================================================== +// Print styles. +// Inlined to avoid the additional HTTP request: h5bp.com/r +// ========================================================================== + +@media print { + *, + *:before, + *:after { + background: transparent !important; + color: #000 !important; // Black prints faster: h5bp.com/s + box-shadow: none !important; + text-shadow: none !important; + } + + a, + a:visited { + text-decoration: underline; + } + + a[href]:after { + content: " (" attr(href) ")"; + } + + abbr[title]:after { + content: " (" attr(title) ")"; + } + + // Don't show links that are fragment identifiers, + // or use the `javascript:` pseudo protocol + a[href^="#"]:after, + a[href^="javascript:"]:after { + content: ""; + } + + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + + thead { + display: table-header-group; // h5bp.com/t + } + + tr, + img { + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + } + + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + + h2, + h3 { + page-break-after: avoid; + } + + // Bootstrap specific changes start + + // Bootstrap components + .navbar { + display: none; + } + .btn, + .dropup > .btn { + > .caret { + border-top-color: #000 !important; + } + } + .label { + border: 1px solid #000; + } + + .table { + border-collapse: collapse !important; + + td, + th { + background-color: #fff !important; + } + } + .table-bordered { + th, + td { + border: 1px solid #ddd !important; + } + } + + // Bootstrap specific changes end +} diff --git a/frontend/css/vendors/less/progress-bars.less b/frontend/css/vendors/less/progress-bars.less new file mode 100644 index 0000000..8868a1f --- /dev/null +++ b/frontend/css/vendors/less/progress-bars.less @@ -0,0 +1,87 @@ +// +// Progress bars +// -------------------------------------------------- + + +// Bar animations +// ------------------------- + +// WebKit +@-webkit-keyframes progress-bar-stripes { + from { background-position: 40px 0; } + to { background-position: 0 0; } +} + +// Spec and IE10+ +@keyframes progress-bar-stripes { + from { background-position: 40px 0; } + to { background-position: 0 0; } +} + + +// Bar itself +// ------------------------- + +// Outer container +.progress { + overflow: hidden; + height: @line-height-computed; + margin-bottom: @line-height-computed; + background-color: @progress-bg; + border-radius: @progress-border-radius; + .box-shadow(inset 0 1px 2px rgba(0,0,0,.1)); +} + +// Bar of progress +.progress-bar { + float: left; + width: 0%; + height: 100%; + font-size: @font-size-small; + line-height: @line-height-computed; + color: @progress-bar-color; + text-align: center; + background-color: @progress-bar-bg; + .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15)); + .transition(width .6s ease); +} + +// Striped bars +// +// `.progress-striped .progress-bar` is deprecated as of v3.2.0 in favor of the +// `.progress-bar-striped` class, which you just add to an existing +// `.progress-bar`. +.progress-striped .progress-bar, +.progress-bar-striped { + #gradient > .striped(); + background-size: 40px 40px; +} + +// Call animation for the active one +// +// `.progress.active .progress-bar` is deprecated as of v3.2.0 in favor of the +// `.progress-bar.active` approach. +.progress.active .progress-bar, +.progress-bar.active { + .animation(progress-bar-stripes 2s linear infinite); +} + + +// Variations +// ------------------------- + +.progress-bar-success { + .progress-bar-variant(@progress-bar-success-bg); +} + +.progress-bar-info { + .progress-bar-variant(@progress-bar-info-bg); +} + +.progress-bar-warning { + .progress-bar-variant(@progress-bar-warning-bg); +} + +.progress-bar-danger { + .progress-bar-variant(@progress-bar-danger-bg); +} diff --git a/frontend/css/vendors/less/responsive-embed.less b/frontend/css/vendors/less/responsive-embed.less new file mode 100644 index 0000000..080a511 --- /dev/null +++ b/frontend/css/vendors/less/responsive-embed.less @@ -0,0 +1,35 @@ +// Embeds responsive +// +// Credit: Nicolas Gallagher and SUIT CSS. + +.embed-responsive { + position: relative; + display: block; + height: 0; + padding: 0; + overflow: hidden; + + .embed-responsive-item, + iframe, + embed, + object, + video { + position: absolute; + top: 0; + left: 0; + bottom: 0; + height: 100%; + width: 100%; + border: 0; + } +} + +// Modifier class for 16:9 aspect ratio +.embed-responsive-16by9 { + padding-bottom: 56.25%; +} + +// Modifier class for 4:3 aspect ratio +.embed-responsive-4by3 { + padding-bottom: 75%; +} diff --git a/frontend/css/vendors/less/responsive-utilities.less b/frontend/css/vendors/less/responsive-utilities.less new file mode 100644 index 0000000..b1db31d --- /dev/null +++ b/frontend/css/vendors/less/responsive-utilities.less @@ -0,0 +1,194 @@ +// +// Responsive: Utility classes +// -------------------------------------------------- + + +// IE10 in Windows (Phone) 8 +// +// Support for responsive views via media queries is kind of borked in IE10, for +// Surface/desktop in split view and for Windows Phone 8. This particular fix +// must be accompanied by a snippet of JavaScript to sniff the user agent and +// apply some conditional CSS to *only* the Surface/desktop Windows 8. Look at +// our Getting Started page for more information on this bug. +// +// For more information, see the following: +// +// Issue: https://github.com/twbs/bootstrap/issues/10497 +// Docs: http://getbootstrap.com/getting-started/#support-ie10-width +// Source: http://timkadlec.com/2013/01/windows-phone-8-and-device-width/ +// Source: http://timkadlec.com/2012/10/ie10-snap-mode-and-responsive-design/ + +@-ms-viewport { + width: device-width; +} + + +// Visibility utilities +// Note: Deprecated .visible-xs, .visible-sm, .visible-md, and .visible-lg as of v3.2.0 +.visible-xs, +.visible-sm, +.visible-md, +.visible-lg { + .responsive-invisibility(); +} + +.visible-xs-block, +.visible-xs-inline, +.visible-xs-inline-block, +.visible-sm-block, +.visible-sm-inline, +.visible-sm-inline-block, +.visible-md-block, +.visible-md-inline, +.visible-md-inline-block, +.visible-lg-block, +.visible-lg-inline, +.visible-lg-inline-block { + display: none !important; +} + +.visible-xs { + @media (max-width: @screen-xs-max) { + .responsive-visibility(); + } +} +.visible-xs-block { + @media (max-width: @screen-xs-max) { + display: block !important; + } +} +.visible-xs-inline { + @media (max-width: @screen-xs-max) { + display: inline !important; + } +} +.visible-xs-inline-block { + @media (max-width: @screen-xs-max) { + display: inline-block !important; + } +} + +.visible-sm { + @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { + .responsive-visibility(); + } +} +.visible-sm-block { + @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { + display: block !important; + } +} +.visible-sm-inline { + @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { + display: inline !important; + } +} +.visible-sm-inline-block { + @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { + display: inline-block !important; + } +} + +.visible-md { + @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { + .responsive-visibility(); + } +} +.visible-md-block { + @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { + display: block !important; + } +} +.visible-md-inline { + @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { + display: inline !important; + } +} +.visible-md-inline-block { + @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { + display: inline-block !important; + } +} + +.visible-lg { + @media (min-width: @screen-lg-min) { + .responsive-visibility(); + } +} +.visible-lg-block { + @media (min-width: @screen-lg-min) { + display: block !important; + } +} +.visible-lg-inline { + @media (min-width: @screen-lg-min) { + display: inline !important; + } +} +.visible-lg-inline-block { + @media (min-width: @screen-lg-min) { + display: inline-block !important; + } +} + +.hidden-xs { + @media (max-width: @screen-xs-max) { + .responsive-invisibility(); + } +} +.hidden-sm { + @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { + .responsive-invisibility(); + } +} +.hidden-md { + @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { + .responsive-invisibility(); + } +} +.hidden-lg { + @media (min-width: @screen-lg-min) { + .responsive-invisibility(); + } +} + + +// Print utilities +// +// Media queries are placed on the inside to be mixin-friendly. + +// Note: Deprecated .visible-print as of v3.2.0 +.visible-print { + .responsive-invisibility(); + + @media print { + .responsive-visibility(); + } +} +.visible-print-block { + display: none !important; + + @media print { + display: block !important; + } +} +.visible-print-inline { + display: none !important; + + @media print { + display: inline !important; + } +} +.visible-print-inline-block { + display: none !important; + + @media print { + display: inline-block !important; + } +} + +.hidden-print { + @media print { + .responsive-invisibility(); + } +} diff --git a/frontend/css/vendors/less/scaffolding.less b/frontend/css/vendors/less/scaffolding.less new file mode 100644 index 0000000..1929bfc --- /dev/null +++ b/frontend/css/vendors/less/scaffolding.less @@ -0,0 +1,161 @@ +// +// Scaffolding +// -------------------------------------------------- + + +// Reset the box-sizing +// +// Heads up! This reset may cause conflicts with some third-party widgets. +// For recommendations on resolving such conflicts, see +// http://getbootstrap.com/getting-started/#third-box-sizing +* { + .box-sizing(border-box); +} +*:before, +*:after { + .box-sizing(border-box); +} + + +// Body reset + +html { + font-size: 10px; + -webkit-tap-highlight-color: rgba(0,0,0,0); +} + +body { + font-family: @font-family-base; + font-size: @font-size-base; + line-height: @line-height-base; + color: @text-color; + background-color: @body-bg; +} + +// Reset fonts for relevant elements +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + + +// Links + +a { + color: @link-color; + text-decoration: none; + + &:hover, + &:focus { + color: @link-hover-color; + text-decoration: @link-hover-decoration; + } + + &:focus { + .tab-focus(); + } +} + + +// Figures +// +// We reset this here because previously Normalize had no `figure` margins. This +// ensures we don't break anyone's use of the element. + +figure { + margin: 0; +} + + +// Images + +img { + vertical-align: middle; +} + +// Responsive images (ensure images don't scale beyond their parents) +.img-responsive { + .img-responsive(); +} + +// Rounded corners +.img-rounded { + border-radius: @border-radius-large; +} + +// Image thumbnails +// +// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`. +.img-thumbnail { + padding: @thumbnail-padding; + line-height: @line-height-base; + background-color: @thumbnail-bg; + border: 1px solid @thumbnail-border; + border-radius: @thumbnail-border-radius; + .transition(all .2s ease-in-out); + + // Keep them at most 100% wide + .img-responsive(inline-block); +} + +// Perfect circle +.img-circle { + border-radius: 50%; // set radius in percents +} + + +// Horizontal rules + +hr { + margin-top: @line-height-computed; + margin-bottom: @line-height-computed; + border: 0; + border-top: 1px solid @hr-border; +} + + +// Only display content to screen readers +// +// See: http://a11yproject.com/posts/how-to-hide-content/ + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; +} + +// Use in conjunction with .sr-only to only display content when it's focused. +// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 +// Credit: HTML5 Boilerplate + +.sr-only-focusable { + &:active, + &:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; + } +} + + +// iOS "clickable elements" fix for role="button" +// +// Fixes "clickability" issue (and more generally, the firing of events such as focus as well) +// for traditionally non-focusable elements with role="button" +// see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile + +[role="button"] { + cursor: pointer; +} diff --git a/frontend/css/vendors/less/tables.less b/frontend/css/vendors/less/tables.less new file mode 100644 index 0000000..2242c03 --- /dev/null +++ b/frontend/css/vendors/less/tables.less @@ -0,0 +1,234 @@ +// +// Tables +// -------------------------------------------------- + + +table { + background-color: @table-bg; +} +caption { + padding-top: @table-cell-padding; + padding-bottom: @table-cell-padding; + color: @text-muted; + text-align: left; +} +th { + text-align: left; +} + + +// Baseline styles + +.table { + width: 100%; + max-width: 100%; + margin-bottom: @line-height-computed; + // Cells + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + padding: @table-cell-padding; + line-height: @line-height-base; + vertical-align: top; + border-top: 1px solid @table-border-color; + } + } + } + // Bottom align for column headings + > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid @table-border-color; + } + // Remove top border from thead by default + > caption + thead, + > colgroup + thead, + > thead:first-child { + > tr:first-child { + > th, + > td { + border-top: 0; + } + } + } + // Account for multiple tbody instances + > tbody + tbody { + border-top: 2px solid @table-border-color; + } + + // Nesting + .table { + background-color: @body-bg; + } +} + + +// Condensed table w/ half padding + +.table-condensed { + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + padding: @table-condensed-cell-padding; + } + } + } +} + + +// Bordered version +// +// Add borders all around the table and between all the columns. + +.table-bordered { + border: 1px solid @table-border-color; + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + border: 1px solid @table-border-color; + } + } + } + > thead > tr { + > th, + > td { + border-bottom-width: 2px; + } + } +} + + +// Zebra-striping +// +// Default zebra-stripe styles (alternating gray and transparent backgrounds) + +.table-striped { + > tbody > tr:nth-of-type(odd) { + background-color: @table-bg-accent; + } +} + + +// Hover effect +// +// Placed here since it has to come after the potential zebra striping + +.table-hover { + > tbody > tr:hover { + background-color: @table-bg-hover; + } +} + + +// Table cell sizing +// +// Reset default table behavior + +table col[class*="col-"] { + position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623) + float: none; + display: table-column; +} +table { + td, + th { + &[class*="col-"] { + position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623) + float: none; + display: table-cell; + } + } +} + + +// Table backgrounds +// +// Exact selectors below required to override `.table-striped` and prevent +// inheritance to nested tables. + +// Generate the contextual variants +.table-row-variant(active; @table-bg-active); +.table-row-variant(success; @state-success-bg); +.table-row-variant(info; @state-info-bg); +.table-row-variant(warning; @state-warning-bg); +.table-row-variant(danger; @state-danger-bg); + + +// Responsive tables +// +// Wrap your tables in `.table-responsive` and we'll make them mobile friendly +// by enabling horizontal scrolling. Only applies <768px. Everything above that +// will display normally. + +.table-responsive { + overflow-x: auto; + min-height: 0.01%; // Workaround for IE9 bug (see https://github.com/twbs/bootstrap/issues/14837) + + @media screen and (max-width: @screen-xs-max) { + width: 100%; + margin-bottom: (@line-height-computed * 0.75); + overflow-y: hidden; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid @table-border-color; + + // Tighten up spacing + > .table { + margin-bottom: 0; + + // Ensure the content doesn't wrap + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + white-space: nowrap; + } + } + } + } + + // Special overrides for the bordered tables + > .table-bordered { + border: 0; + + // Nuke the appropriate borders so that the parent can handle them + > thead, + > tbody, + > tfoot { + > tr { + > th:first-child, + > td:first-child { + border-left: 0; + } + > th:last-child, + > td:last-child { + border-right: 0; + } + } + } + + // Only nuke the last row's bottom-border in `tbody` and `tfoot` since + // chances are there will be only one `tr` in a `thead` and that would + // remove the border altogether. + > tbody, + > tfoot { + > tr:last-child { + > th, + > td { + border-bottom: 0; + } + } + } + + } + } +} diff --git a/frontend/css/vendors/less/theme.less b/frontend/css/vendors/less/theme.less new file mode 100644 index 0000000..8f51d91 --- /dev/null +++ b/frontend/css/vendors/less/theme.less @@ -0,0 +1,291 @@ +/*! + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +// +// Load core variables and mixins +// -------------------------------------------------- + +@import "variables.less"; +@import "mixins.less"; + + +// +// Buttons +// -------------------------------------------------- + +// Common styles +.btn-default, +.btn-primary, +.btn-success, +.btn-info, +.btn-warning, +.btn-danger { + text-shadow: 0 -1px 0 rgba(0,0,0,.2); + @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075); + .box-shadow(@shadow); + + // Reset the shadow + &:active, + &.active { + .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + } + + &.disabled, + &[disabled], + fieldset[disabled] & { + .box-shadow(none); + } + + .badge { + text-shadow: none; + } +} + +// Mixin for generating new styles +.btn-styles(@btn-color: #555) { + #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%)); + .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners; see https://github.com/twbs/bootstrap/issues/10620 + background-repeat: repeat-x; + border-color: darken(@btn-color, 14%); + + &:hover, + &:focus { + background-color: darken(@btn-color, 12%); + background-position: 0 -15px; + } + + &:active, + &.active { + background-color: darken(@btn-color, 12%); + border-color: darken(@btn-color, 14%); + } + + &.disabled, + &[disabled], + fieldset[disabled] & { + &, + &:hover, + &:focus, + &.focus, + &:active, + &.active { + background-color: darken(@btn-color, 12%); + background-image: none; + } + } +} + +// Common styles +.btn { + // Remove the gradient for the pressed/active state + &:active, + &.active { + background-image: none; + } +} + +// Apply the mixin to the buttons +.btn-default { .btn-styles(@btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; } +.btn-primary { .btn-styles(@btn-primary-bg); } +.btn-success { .btn-styles(@btn-success-bg); } +.btn-info { .btn-styles(@btn-info-bg); } +.btn-warning { .btn-styles(@btn-warning-bg); } +.btn-danger { .btn-styles(@btn-danger-bg); } + + +// +// Images +// -------------------------------------------------- + +.thumbnail, +.img-thumbnail { + .box-shadow(0 1px 2px rgba(0,0,0,.075)); +} + + +// +// Dropdowns +// -------------------------------------------------- + +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%)); + background-color: darken(@dropdown-link-hover-bg, 5%); +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%)); + background-color: darken(@dropdown-link-active-bg, 5%); +} + + +// +// Navbar +// -------------------------------------------------- + +// Default navbar +.navbar-default { + #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg); + .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered + border-radius: @navbar-border-radius; + @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075); + .box-shadow(@shadow); + + .navbar-nav > .open > a, + .navbar-nav > .active > a { + #gradient > .vertical(@start-color: darken(@navbar-default-link-active-bg, 5%); @end-color: darken(@navbar-default-link-active-bg, 2%)); + .box-shadow(inset 0 3px 9px rgba(0,0,0,.075)); + } +} +.navbar-brand, +.navbar-nav > li > a { + text-shadow: 0 1px 0 rgba(255,255,255,.25); +} + +// Inverted navbar +.navbar-inverse { + #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg); + .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered; see https://github.com/twbs/bootstrap/issues/10257 + border-radius: @navbar-border-radius; + .navbar-nav > .open > a, + .navbar-nav > .active > a { + #gradient > .vertical(@start-color: @navbar-inverse-link-active-bg; @end-color: lighten(@navbar-inverse-link-active-bg, 2.5%)); + .box-shadow(inset 0 3px 9px rgba(0,0,0,.25)); + } + + .navbar-brand, + .navbar-nav > li > a { + text-shadow: 0 -1px 0 rgba(0,0,0,.25); + } +} + +// Undo rounded corners in static and fixed navbars +.navbar-static-top, +.navbar-fixed-top, +.navbar-fixed-bottom { + border-radius: 0; +} + +// Fix active state of dropdown items in collapsed mode +@media (max-width: @grid-float-breakpoint-max) { + .navbar .navbar-nav .open .dropdown-menu > .active > a { + &, + &:hover, + &:focus { + color: #fff; + #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%)); + } + } +} + + +// +// Alerts +// -------------------------------------------------- + +// Common styles +.alert { + text-shadow: 0 1px 0 rgba(255,255,255,.2); + @shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05); + .box-shadow(@shadow); +} + +// Mixin for generating new styles +.alert-styles(@color) { + #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%)); + border-color: darken(@color, 15%); +} + +// Apply the mixin to the alerts +.alert-success { .alert-styles(@alert-success-bg); } +.alert-info { .alert-styles(@alert-info-bg); } +.alert-warning { .alert-styles(@alert-warning-bg); } +.alert-danger { .alert-styles(@alert-danger-bg); } + + +// +// Progress bars +// -------------------------------------------------- + +// Give the progress background some depth +.progress { + #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg) +} + +// Mixin for generating new styles +.progress-bar-styles(@color) { + #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%)); +} + +// Apply the mixin to the progress bars +.progress-bar { .progress-bar-styles(@progress-bar-bg); } +.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); } +.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); } +.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); } +.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); } + +// Reset the striped class because our mixins don't do multiple gradients and +// the above custom styles override the new `.progress-bar-striped` in v3.2.0. +.progress-bar-striped { + #gradient > .striped(); +} + + +// +// List groups +// -------------------------------------------------- + +.list-group { + border-radius: @border-radius-base; + .box-shadow(0 1px 2px rgba(0,0,0,.075)); +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%); + #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%)); + border-color: darken(@list-group-active-border, 7.5%); + + .badge { + text-shadow: none; + } +} + + +// +// Panels +// -------------------------------------------------- + +// Common styles +.panel { + .box-shadow(0 1px 2px rgba(0,0,0,.05)); +} + +// Mixin for generating new styles +.panel-heading-styles(@color) { + #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%)); +} + +// Apply the mixin to the panel headings only +.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); } +.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); } +.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); } +.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); } +.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); } +.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); } + + +// +// Wells +// -------------------------------------------------- + +.well { + #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg); + border-color: darken(@well-bg, 10%); + @shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1); + .box-shadow(@shadow); +} diff --git a/frontend/css/vendors/less/thumbnails.less b/frontend/css/vendors/less/thumbnails.less new file mode 100644 index 0000000..0713e67 --- /dev/null +++ b/frontend/css/vendors/less/thumbnails.less @@ -0,0 +1,36 @@ +// +// Thumbnails +// -------------------------------------------------- + + +// Mixin and adjust the regular image class +.thumbnail { + display: block; + padding: @thumbnail-padding; + margin-bottom: @line-height-computed; + line-height: @line-height-base; + background-color: @thumbnail-bg; + border: 1px solid @thumbnail-border; + border-radius: @thumbnail-border-radius; + .transition(border .2s ease-in-out); + + > img, + a > img { + &:extend(.img-responsive); + margin-left: auto; + margin-right: auto; + } + + // Add a hover state for linked versions only + a&:hover, + a&:focus, + a&.active { + border-color: @link-color; + } + + // Image captions + .caption { + padding: @thumbnail-caption-padding; + color: @thumbnail-caption-color; + } +} diff --git a/frontend/css/vendors/less/tooltip.less b/frontend/css/vendors/less/tooltip.less new file mode 100644 index 0000000..b48d63e --- /dev/null +++ b/frontend/css/vendors/less/tooltip.less @@ -0,0 +1,101 @@ +// +// Tooltips +// -------------------------------------------------- + + +// Base class +.tooltip { + position: absolute; + z-index: @zindex-tooltip; + display: block; + // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element. + // So reset our font and text properties to avoid inheriting weird values. + .reset-text(); + font-size: @font-size-small; + + .opacity(0); + + &.in { .opacity(@tooltip-opacity); } + &.top { margin-top: -3px; padding: @tooltip-arrow-width 0; } + &.right { margin-left: 3px; padding: 0 @tooltip-arrow-width; } + &.bottom { margin-top: 3px; padding: @tooltip-arrow-width 0; } + &.left { margin-left: -3px; padding: 0 @tooltip-arrow-width; } +} + +// Wrapper for the tooltip content +.tooltip-inner { + max-width: @tooltip-max-width; + padding: 3px 8px; + color: @tooltip-color; + text-align: center; + background-color: @tooltip-bg; + border-radius: @border-radius-base; +} + +// Arrows +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +// Note: Deprecated .top-left, .top-right, .bottom-left, and .bottom-right as of v3.3.1 +.tooltip { + &.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -@tooltip-arrow-width; + border-width: @tooltip-arrow-width @tooltip-arrow-width 0; + border-top-color: @tooltip-arrow-color; + } + &.top-left .tooltip-arrow { + bottom: 0; + right: @tooltip-arrow-width; + margin-bottom: -@tooltip-arrow-width; + border-width: @tooltip-arrow-width @tooltip-arrow-width 0; + border-top-color: @tooltip-arrow-color; + } + &.top-right .tooltip-arrow { + bottom: 0; + left: @tooltip-arrow-width; + margin-bottom: -@tooltip-arrow-width; + border-width: @tooltip-arrow-width @tooltip-arrow-width 0; + border-top-color: @tooltip-arrow-color; + } + &.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -@tooltip-arrow-width; + border-width: @tooltip-arrow-width @tooltip-arrow-width @tooltip-arrow-width 0; + border-right-color: @tooltip-arrow-color; + } + &.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -@tooltip-arrow-width; + border-width: @tooltip-arrow-width 0 @tooltip-arrow-width @tooltip-arrow-width; + border-left-color: @tooltip-arrow-color; + } + &.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -@tooltip-arrow-width; + border-width: 0 @tooltip-arrow-width @tooltip-arrow-width; + border-bottom-color: @tooltip-arrow-color; + } + &.bottom-left .tooltip-arrow { + top: 0; + right: @tooltip-arrow-width; + margin-top: -@tooltip-arrow-width; + border-width: 0 @tooltip-arrow-width @tooltip-arrow-width; + border-bottom-color: @tooltip-arrow-color; + } + &.bottom-right .tooltip-arrow { + top: 0; + left: @tooltip-arrow-width; + margin-top: -@tooltip-arrow-width; + border-width: 0 @tooltip-arrow-width @tooltip-arrow-width; + border-bottom-color: @tooltip-arrow-color; + } +} diff --git a/frontend/css/vendors/less/type.less b/frontend/css/vendors/less/type.less new file mode 100644 index 0000000..0d4fee4 --- /dev/null +++ b/frontend/css/vendors/less/type.less @@ -0,0 +1,302 @@ +// +// Typography +// -------------------------------------------------- + + +// Headings +// ------------------------- + +h1, h2, h3, h4, h5, h6, +.h1, .h2, .h3, .h4, .h5, .h6 { + font-family: @headings-font-family; + font-weight: @headings-font-weight; + line-height: @headings-line-height; + color: @headings-color; + + small, + .small { + font-weight: normal; + line-height: 1; + color: @headings-small-color; + } +} + +h1, .h1, +h2, .h2, +h3, .h3 { + margin-top: @line-height-computed; + margin-bottom: (@line-height-computed / 2); + + small, + .small { + font-size: 65%; + } +} +h4, .h4, +h5, .h5, +h6, .h6 { + margin-top: (@line-height-computed / 2); + margin-bottom: (@line-height-computed / 2); + + small, + .small { + font-size: 75%; + } +} + +h1, .h1 { font-size: @font-size-h1; } +h2, .h2 { font-size: @font-size-h2; } +h3, .h3 { font-size: @font-size-h3; } +h4, .h4 { font-size: @font-size-h4; } +h5, .h5 { font-size: @font-size-h5; } +h6, .h6 { font-size: @font-size-h6; } + + +// Body text +// ------------------------- + +p { + margin: 0 0 (@line-height-computed / 2); +} + +.lead { + margin-bottom: @line-height-computed; + font-size: floor((@font-size-base * 1.15)); + font-weight: 300; + line-height: 1.4; + + @media (min-width: @screen-sm-min) { + font-size: (@font-size-base * 1.5); + } +} + + +// Emphasis & misc +// ------------------------- + +// Ex: (12px small font / 14px base font) * 100% = about 85% +small, +.small { + font-size: floor((100% * @font-size-small / @font-size-base)); +} + +mark, +.mark { + background-color: @state-warning-bg; + padding: .2em; +} + +// Alignment +.text-left { text-align: left; } +.text-right { text-align: right; } +.text-center { text-align: center; } +.text-justify { text-align: justify; } +.text-nowrap { white-space: nowrap; } + +// Transformation +.text-lowercase { text-transform: lowercase; } +.text-uppercase { text-transform: uppercase; } +.text-capitalize { text-transform: capitalize; } + +// Contextual colors +.text-muted { + color: @text-muted; +} +.text-primary { + .text-emphasis-variant(@brand-primary); +} +.text-success { + .text-emphasis-variant(@state-success-text); +} +.text-info { + .text-emphasis-variant(@state-info-text); +} +.text-warning { + .text-emphasis-variant(@state-warning-text); +} +.text-danger { + .text-emphasis-variant(@state-danger-text); +} + +// Contextual backgrounds +// For now we'll leave these alongside the text classes until v4 when we can +// safely shift things around (per SemVer rules). +.bg-primary { + // Given the contrast here, this is the only class to have its color inverted + // automatically. + color: #fff; + .bg-variant(@brand-primary); +} +.bg-success { + .bg-variant(@state-success-bg); +} +.bg-info { + .bg-variant(@state-info-bg); +} +.bg-warning { + .bg-variant(@state-warning-bg); +} +.bg-danger { + .bg-variant(@state-danger-bg); +} + + +// Page header +// ------------------------- + +.page-header { + padding-bottom: ((@line-height-computed / 2) - 1); + margin: (@line-height-computed * 2) 0 @line-height-computed; + border-bottom: 1px solid @page-header-border-color; +} + + +// Lists +// ------------------------- + +// Unordered and Ordered lists +ul, +ol { + margin-top: 0; + margin-bottom: (@line-height-computed / 2); + ul, + ol { + margin-bottom: 0; + } +} + +// List options + +// Unstyled keeps list items block level, just removes default browser padding and list-style +.list-unstyled { + padding-left: 0; + list-style: none; +} + +// Inline turns list items into inline-block +.list-inline { + .list-unstyled(); + margin-left: -5px; + + > li { + display: inline-block; + padding-left: 5px; + padding-right: 5px; + } +} + +// Description Lists +dl { + margin-top: 0; // Remove browser default + margin-bottom: @line-height-computed; +} +dt, +dd { + line-height: @line-height-base; +} +dt { + font-weight: bold; +} +dd { + margin-left: 0; // Undo browser default +} + +// Horizontal description lists +// +// Defaults to being stacked without any of the below styles applied, until the +// grid breakpoint is reached (default of ~768px). + +.dl-horizontal { + dd { + &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present + } + + @media (min-width: @dl-horizontal-breakpoint) { + dt { + float: left; + width: (@dl-horizontal-offset - 20); + clear: left; + text-align: right; + .text-overflow(); + } + dd { + margin-left: @dl-horizontal-offset; + } + } +} + + +// Misc +// ------------------------- + +// Abbreviations and acronyms +abbr[title], +// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257 +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted @abbr-border-color; +} +.initialism { + font-size: 90%; + .text-uppercase(); +} + +// Blockquotes +blockquote { + padding: (@line-height-computed / 2) @line-height-computed; + margin: 0 0 @line-height-computed; + font-size: @blockquote-font-size; + border-left: 5px solid @blockquote-border-color; + + p, + ul, + ol { + &:last-child { + margin-bottom: 0; + } + } + + // Note: Deprecated small and .small as of v3.1.0 + // Context: https://github.com/twbs/bootstrap/issues/11660 + footer, + small, + .small { + display: block; + font-size: 80%; // back to default font-size + line-height: @line-height-base; + color: @blockquote-small-color; + + &:before { + content: '\2014 \00A0'; // em dash, nbsp + } + } +} + +// Opposite alignment of blockquote +// +// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0. +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid @blockquote-border-color; + border-left: 0; + text-align: right; + + // Account for citation + footer, + small, + .small { + &:before { content: ''; } + &:after { + content: '\00A0 \2014'; // nbsp, em dash + } + } +} + +// Addresses +address { + margin-bottom: @line-height-computed; + font-style: normal; + line-height: @line-height-base; +} diff --git a/frontend/css/vendors/less/utilities.less b/frontend/css/vendors/less/utilities.less new file mode 100644 index 0000000..7a8ca27 --- /dev/null +++ b/frontend/css/vendors/less/utilities.less @@ -0,0 +1,55 @@ +// +// Utility classes +// -------------------------------------------------- + + +// Floats +// ------------------------- + +.clearfix { + .clearfix(); +} +.center-block { + .center-block(); +} +.pull-right { + float: right !important; +} +.pull-left { + float: left !important; +} + + +// Toggling content +// ------------------------- + +// Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1 +.hide { + display: none !important; +} +.show { + display: block !important; +} +.invisible { + visibility: hidden; +} +.text-hide { + .text-hide(); +} + + +// Hide from screenreaders and browsers +// +// Credit: HTML5 Boilerplate + +.hidden { + display: none !important; +} + + +// For Affix plugin +// ------------------------- + +.affix { + position: fixed; +} diff --git a/frontend/css/vendors/less/variables.less b/frontend/css/vendors/less/variables.less new file mode 100644 index 0000000..b057ef5 --- /dev/null +++ b/frontend/css/vendors/less/variables.less @@ -0,0 +1,869 @@ +// +// Variables +// -------------------------------------------------- + + +//== Colors +// +//## Gray and brand colors for use across Bootstrap. + +@gray-base: #000; +@gray-darker: lighten(@gray-base, 13.5%); // #222 +@gray-dark: lighten(@gray-base, 20%); // #333 +@gray: lighten(@gray-base, 33.5%); // #555 +@gray-light: lighten(@gray-base, 46.7%); // #777 +@gray-lighter: lighten(@gray-base, 93.5%); // #eee + +@brand-primary: darken(#428bca, 6.5%); // #337ab7 +@brand-success: #5cb85c; +@brand-info: #5bc0de; +@brand-warning: #f0ad4e; +@brand-danger: #d9534f; + + +//== Scaffolding +// +//## Settings for some of the most global styles. + +//** Background color for ``. +@body-bg: #fff; +//** Global text color on ``. +@text-color: @gray-dark; + +//** Global textual link color. +@link-color: @brand-primary; +//** Link hover color set via `darken()` function. +@link-hover-color: darken(@link-color, 15%); +//** Link hover decoration. +@link-hover-decoration: underline; + + +//== Typography +// +//## Font, line-height, and color for body text, headings, and more. + +@font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif; +@font-family-serif: Georgia, "Times New Roman", Times, serif; +//** Default monospace fonts for ``, ``, and `
`.
+@font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace;
+@font-family-base:        @font-family-sans-serif;
+
+@font-size-base:          14px;
+@font-size-large:         ceil((@font-size-base * 1.25)); // ~18px
+@font-size-small:         ceil((@font-size-base * 0.85)); // ~12px
+
+@font-size-h1:            floor((@font-size-base * 2.6)); // ~36px
+@font-size-h2:            floor((@font-size-base * 2.15)); // ~30px
+@font-size-h3:            ceil((@font-size-base * 1.7)); // ~24px
+@font-size-h4:            ceil((@font-size-base * 1.25)); // ~18px
+@font-size-h5:            @font-size-base;
+@font-size-h6:            ceil((@font-size-base * 0.85)); // ~12px
+
+//** Unit-less `line-height` for use in components like buttons.
+@line-height-base:        1.428571429; // 20/14
+//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
+@line-height-computed:    floor((@font-size-base * @line-height-base)); // ~20px
+
+//** By default, this inherits from the ``.
+@headings-font-family:    inherit;
+@headings-font-weight:    500;
+@headings-line-height:    1.1;
+@headings-color:          inherit;
+
+
+//== Iconography
+//
+//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
+
+//** Load fonts from this directory.
+@icon-font-path:          "../fonts/";
+//** File name for all font files.
+@icon-font-name:          "glyphicons-halflings-regular";
+//** Element ID within SVG icon file.
+@icon-font-svg-id:        "glyphicons_halflingsregular";
+
+
+//== Components
+//
+//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
+
+@padding-base-vertical:     6px;
+@padding-base-horizontal:   12px;
+
+@padding-large-vertical:    10px;
+@padding-large-horizontal:  16px;
+
+@padding-small-vertical:    5px;
+@padding-small-horizontal:  10px;
+
+@padding-xs-vertical:       1px;
+@padding-xs-horizontal:     5px;
+
+@line-height-large:         1.3333333; // extra decimals for Win 8.1 Chrome
+@line-height-small:         1.5;
+
+@border-radius-base:        4px;
+@border-radius-large:       6px;
+@border-radius-small:       3px;
+
+//** Global color for active items (e.g., navs or dropdowns).
+@component-active-color:    #fff;
+//** Global background color for active items (e.g., navs or dropdowns).
+@component-active-bg:       @brand-primary;
+
+//** Width of the `border` for generating carets that indicator dropdowns.
+@caret-width-base:          4px;
+//** Carets increase slightly in size for larger components.
+@caret-width-large:         5px;
+
+
+//== Tables
+//
+//## Customizes the `.table` component with basic values, each used across all table variations.
+
+//** Padding for ``s and ``s.
+@table-cell-padding:            8px;
+//** Padding for cells in `.table-condensed`.
+@table-condensed-cell-padding:  5px;
+
+//** Default background color used for all tables.
+@table-bg:                      transparent;
+//** Background color used for `.table-striped`.
+@table-bg-accent:               #f9f9f9;
+//** Background color used for `.table-hover`.
+@table-bg-hover:                #f5f5f5;
+@table-bg-active:               @table-bg-hover;
+
+//** Border color for table and cell borders.
+@table-border-color:            #ddd;
+
+
+//== Buttons
+//
+//## For each of Bootstrap's buttons, define text, background and border color.
+
+@btn-font-weight:                normal;
+
+@btn-default-color:              #333;
+@btn-default-bg:                 #fff;
+@btn-default-border:             #ccc;
+
+@btn-primary-color:              #fff;
+@btn-primary-bg:                 @brand-primary;
+@btn-primary-border:             darken(@btn-primary-bg, 5%);
+
+@btn-success-color:              #fff;
+@btn-success-bg:                 @brand-success;
+@btn-success-border:             darken(@btn-success-bg, 5%);
+
+@btn-info-color:                 #fff;
+@btn-info-bg:                    @brand-info;
+@btn-info-border:                darken(@btn-info-bg, 5%);
+
+@btn-warning-color:              #fff;
+@btn-warning-bg:                 @brand-warning;
+@btn-warning-border:             darken(@btn-warning-bg, 5%);
+
+@btn-danger-color:               #fff;
+@btn-danger-bg:                  @brand-danger;
+@btn-danger-border:              darken(@btn-danger-bg, 5%);
+
+@btn-link-disabled-color:        @gray-light;
+
+// Allows for customizing button radius independently from global border radius
+@btn-border-radius-base:         @border-radius-base;
+@btn-border-radius-large:        @border-radius-large;
+@btn-border-radius-small:        @border-radius-small;
+
+
+//== Forms
+//
+//##
+
+//** `` background color
+@input-bg:                       #fff;
+//** `` background color
+@input-bg-disabled:              @gray-lighter;
+
+//** Text color for ``s
+@input-color:                    @gray;
+//** `` border color
+@input-border:                   #ccc;
+
+// TODO: Rename `@input-border-radius` to `@input-border-radius-base` in v4
+//** Default `.form-control` border radius
+// This has no effect on ``s in CSS.
+@input-border-radius:            @border-radius-base;
+//** Large `.form-control` border radius
+@input-border-radius-large:      @border-radius-large;
+//** Small `.form-control` border radius
+@input-border-radius-small:      @border-radius-small;
+
+//** Border color for inputs on focus
+@input-border-focus:             #66afe9;
+
+//** Placeholder text color
+@input-color-placeholder:        #999;
+
+//** Default `.form-control` height
+@input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);
+//** Large `.form-control` height
+@input-height-large:             (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);
+//** Small `.form-control` height
+@input-height-small:             (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
+
+//** `.form-group` margin
+@form-group-margin-bottom:       15px;
+
+@legend-color:                   @gray-dark;
+@legend-border-color:            #e5e5e5;
+
+//** Background color for textual input addons
+@input-group-addon-bg:           @gray-lighter;
+//** Border color for textual input addons
+@input-group-addon-border-color: @input-border;
+
+//** Disabled cursor for form controls and buttons.
+@cursor-disabled:                not-allowed;
+
+
+//== Dropdowns
+//
+//## Dropdown menu container and contents.
+
+//** Background for the dropdown menu.
+@dropdown-bg:                    #fff;
+//** Dropdown menu `border-color`.
+@dropdown-border:                rgba(0,0,0,.15);
+//** Dropdown menu `border-color` **for IE8**.
+@dropdown-fallback-border:       #ccc;
+//** Divider color for between dropdown items.
+@dropdown-divider-bg:            #e5e5e5;
+
+//** Dropdown link text color.
+@dropdown-link-color:            @gray-dark;
+//** Hover color for dropdown links.
+@dropdown-link-hover-color:      darken(@gray-dark, 5%);
+//** Hover background for dropdown links.
+@dropdown-link-hover-bg:         #f5f5f5;
+
+//** Active dropdown menu item text color.
+@dropdown-link-active-color:     @component-active-color;
+//** Active dropdown menu item background color.
+@dropdown-link-active-bg:        @component-active-bg;
+
+//** Disabled dropdown menu item background color.
+@dropdown-link-disabled-color:   @gray-light;
+
+//** Text color for headers within dropdown menus.
+@dropdown-header-color:          @gray-light;
+
+//** Deprecated `@dropdown-caret-color` as of v3.1.0
+@dropdown-caret-color:           #000;
+
+
+//-- Z-index master list
+//
+// Warning: Avoid customizing these values. They're used for a bird's eye view
+// of components dependent on the z-axis and are designed to all work together.
+//
+// Note: These variables are not generated into the Customizer.
+
+@zindex-navbar:            1000;
+@zindex-dropdown:          1000;
+@zindex-popover:           1060;
+@zindex-tooltip:           1070;
+@zindex-navbar-fixed:      1030;
+@zindex-modal-background:  1040;
+@zindex-modal:             1050;
+
+
+//== Media queries breakpoints
+//
+//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
+
+// Extra small screen / phone
+//** Deprecated `@screen-xs` as of v3.0.1
+@screen-xs:                  480px;
+//** Deprecated `@screen-xs-min` as of v3.2.0
+@screen-xs-min:              @screen-xs;
+//** Deprecated `@screen-phone` as of v3.0.1
+@screen-phone:               @screen-xs-min;
+
+// Small screen / tablet
+//** Deprecated `@screen-sm` as of v3.0.1
+@screen-sm:                  768px;
+@screen-sm-min:              @screen-sm;
+//** Deprecated `@screen-tablet` as of v3.0.1
+@screen-tablet:              @screen-sm-min;
+
+// Medium screen / desktop
+//** Deprecated `@screen-md` as of v3.0.1
+@screen-md:                  992px;
+@screen-md-min:              @screen-md;
+//** Deprecated `@screen-desktop` as of v3.0.1
+@screen-desktop:             @screen-md-min;
+
+// Large screen / wide desktop
+//** Deprecated `@screen-lg` as of v3.0.1
+@screen-lg:                  1200px;
+@screen-lg-min:              @screen-lg;
+//** Deprecated `@screen-lg-desktop` as of v3.0.1
+@screen-lg-desktop:          @screen-lg-min;
+
+// So media queries don't overlap when required, provide a maximum
+@screen-xs-max:              (@screen-sm-min - 1);
+@screen-sm-max:              (@screen-md-min - 1);
+@screen-md-max:              (@screen-lg-min - 1);
+
+
+//== Grid system
+//
+//## Define your custom responsive grid.
+
+//** Number of columns in the grid.
+@grid-columns:              12;
+//** Padding between columns. Gets divided in half for the left and right.
+@grid-gutter-width:         30px;
+// Navbar collapse
+//** Point at which the navbar becomes uncollapsed.
+@grid-float-breakpoint:     @screen-sm-min;
+//** Point at which the navbar begins collapsing.
+@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
+
+
+//== Container sizes
+//
+//## Define the maximum width of `.container` for different screen sizes.
+
+// Small screen / tablet
+@container-tablet:             (720px + @grid-gutter-width);
+//** For `@screen-sm-min` and up.
+@container-sm:                 @container-tablet;
+
+// Medium screen / desktop
+@container-desktop:            (940px + @grid-gutter-width);
+//** For `@screen-md-min` and up.
+@container-md:                 @container-desktop;
+
+// Large screen / wide desktop
+@container-large-desktop:      (1140px + @grid-gutter-width);
+//** For `@screen-lg-min` and up.
+@container-lg:                 @container-large-desktop;
+
+
+//== Navbar
+//
+//##
+
+// Basics of a navbar
+@navbar-height:                    50px;
+@navbar-margin-bottom:             @line-height-computed;
+@navbar-border-radius:             @border-radius-base;
+@navbar-padding-horizontal:        floor((@grid-gutter-width / 2));
+@navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);
+@navbar-collapse-max-height:       340px;
+
+@navbar-default-color:             #777;
+@navbar-default-bg:                #f8f8f8;
+@navbar-default-border:            darken(@navbar-default-bg, 6.5%);
+
+// Navbar links
+@navbar-default-link-color:                #777;
+@navbar-default-link-hover-color:          #333;
+@navbar-default-link-hover-bg:             transparent;
+@navbar-default-link-active-color:         #555;
+@navbar-default-link-active-bg:            darken(@navbar-default-bg, 6.5%);
+@navbar-default-link-disabled-color:       #ccc;
+@navbar-default-link-disabled-bg:          transparent;
+
+// Navbar brand label
+@navbar-default-brand-color:               @navbar-default-link-color;
+@navbar-default-brand-hover-color:         darken(@navbar-default-brand-color, 10%);
+@navbar-default-brand-hover-bg:            transparent;
+
+// Navbar toggle
+@navbar-default-toggle-hover-bg:           #ddd;
+@navbar-default-toggle-icon-bar-bg:        #888;
+@navbar-default-toggle-border-color:       #ddd;
+
+
+//=== Inverted navbar
+// Reset inverted navbar basics
+@navbar-inverse-color:                      lighten(@gray-light, 15%);
+@navbar-inverse-bg:                         #222;
+@navbar-inverse-border:                     darken(@navbar-inverse-bg, 10%);
+
+// Inverted navbar links
+@navbar-inverse-link-color:                 lighten(@gray-light, 15%);
+@navbar-inverse-link-hover-color:           #fff;
+@navbar-inverse-link-hover-bg:              transparent;
+@navbar-inverse-link-active-color:          @navbar-inverse-link-hover-color;
+@navbar-inverse-link-active-bg:             darken(@navbar-inverse-bg, 10%);
+@navbar-inverse-link-disabled-color:        #444;
+@navbar-inverse-link-disabled-bg:           transparent;
+
+// Inverted navbar brand label
+@navbar-inverse-brand-color:                @navbar-inverse-link-color;
+@navbar-inverse-brand-hover-color:          #fff;
+@navbar-inverse-brand-hover-bg:             transparent;
+
+// Inverted navbar toggle
+@navbar-inverse-toggle-hover-bg:            #333;
+@navbar-inverse-toggle-icon-bar-bg:         #fff;
+@navbar-inverse-toggle-border-color:        #333;
+
+
+//== Navs
+//
+//##
+
+//=== Shared nav styles
+@nav-link-padding:                          10px 15px;
+@nav-link-hover-bg:                         @gray-lighter;
+
+@nav-disabled-link-color:                   @gray-light;
+@nav-disabled-link-hover-color:             @gray-light;
+
+//== Tabs
+@nav-tabs-border-color:                     #ddd;
+
+@nav-tabs-link-hover-border-color:          @gray-lighter;
+
+@nav-tabs-active-link-hover-bg:             @body-bg;
+@nav-tabs-active-link-hover-color:          @gray;
+@nav-tabs-active-link-hover-border-color:   #ddd;
+
+@nav-tabs-justified-link-border-color:            #ddd;
+@nav-tabs-justified-active-link-border-color:     @body-bg;
+
+//== Pills
+@nav-pills-border-radius:                   @border-radius-base;
+@nav-pills-active-link-hover-bg:            @component-active-bg;
+@nav-pills-active-link-hover-color:         @component-active-color;
+
+
+//== Pagination
+//
+//##
+
+@pagination-color:                     @link-color;
+@pagination-bg:                        #fff;
+@pagination-border:                    #ddd;
+
+@pagination-hover-color:               @link-hover-color;
+@pagination-hover-bg:                  @gray-lighter;
+@pagination-hover-border:              #ddd;
+
+@pagination-active-color:              #fff;
+@pagination-active-bg:                 @brand-primary;
+@pagination-active-border:             @brand-primary;
+
+@pagination-disabled-color:            @gray-light;
+@pagination-disabled-bg:               #fff;
+@pagination-disabled-border:           #ddd;
+
+
+//== Pager
+//
+//##
+
+@pager-bg:                             @pagination-bg;
+@pager-border:                         @pagination-border;
+@pager-border-radius:                  15px;
+
+@pager-hover-bg:                       @pagination-hover-bg;
+
+@pager-active-bg:                      @pagination-active-bg;
+@pager-active-color:                   @pagination-active-color;
+
+@pager-disabled-color:                 @pagination-disabled-color;
+
+
+//== Jumbotron
+//
+//##
+
+@jumbotron-padding:              30px;
+@jumbotron-color:                inherit;
+@jumbotron-bg:                   @gray-lighter;
+@jumbotron-heading-color:        inherit;
+@jumbotron-font-size:            ceil((@font-size-base * 1.5));
+@jumbotron-heading-font-size:    ceil((@font-size-base * 4.5));
+
+
+//== Form states and alerts
+//
+//## Define colors for form feedback states and, by default, alerts.
+
+@state-success-text:             #3c763d;
+@state-success-bg:               #dff0d8;
+@state-success-border:           darken(spin(@state-success-bg, -10), 5%);
+
+@state-info-text:                #31708f;
+@state-info-bg:                  #d9edf7;
+@state-info-border:              darken(spin(@state-info-bg, -10), 7%);
+
+@state-warning-text:             #8a6d3b;
+@state-warning-bg:               #fcf8e3;
+@state-warning-border:           darken(spin(@state-warning-bg, -10), 5%);
+
+@state-danger-text:              #a94442;
+@state-danger-bg:                #f2dede;
+@state-danger-border:            darken(spin(@state-danger-bg, -10), 5%);
+
+
+//== Tooltips
+//
+//##
+
+//** Tooltip max width
+@tooltip-max-width:           200px;
+//** Tooltip text color
+@tooltip-color:               #fff;
+//** Tooltip background color
+@tooltip-bg:                  #000;
+@tooltip-opacity:             .9;
+
+//** Tooltip arrow width
+@tooltip-arrow-width:         5px;
+//** Tooltip arrow color
+@tooltip-arrow-color:         @tooltip-bg;
+
+
+//== Popovers
+//
+//##
+
+//** Popover body background color
+@popover-bg:                          #fff;
+//** Popover maximum width
+@popover-max-width:                   276px;
+//** Popover border color
+@popover-border-color:                rgba(0,0,0,.2);
+//** Popover fallback border color
+@popover-fallback-border-color:       #ccc;
+
+//** Popover title background color
+@popover-title-bg:                    darken(@popover-bg, 3%);
+
+//** Popover arrow width
+@popover-arrow-width:                 10px;
+//** Popover arrow color
+@popover-arrow-color:                 @popover-bg;
+
+//** Popover outer arrow width
+@popover-arrow-outer-width:           (@popover-arrow-width + 1);
+//** Popover outer arrow color
+@popover-arrow-outer-color:           fadein(@popover-border-color, 5%);
+//** Popover outer arrow fallback color
+@popover-arrow-outer-fallback-color:  darken(@popover-fallback-border-color, 20%);
+
+
+//== Labels
+//
+//##
+
+//** Default label background color
+@label-default-bg:            @gray-light;
+//** Primary label background color
+@label-primary-bg:            @brand-primary;
+//** Success label background color
+@label-success-bg:            @brand-success;
+//** Info label background color
+@label-info-bg:               @brand-info;
+//** Warning label background color
+@label-warning-bg:            @brand-warning;
+//** Danger label background color
+@label-danger-bg:             @brand-danger;
+
+//** Default label text color
+@label-color:                 #fff;
+//** Default text color of a linked label
+@label-link-hover-color:      #fff;
+
+
+//== Modals
+//
+//##
+
+//** Padding applied to the modal body
+@modal-inner-padding:         15px;
+
+//** Padding applied to the modal title
+@modal-title-padding:         15px;
+//** Modal title line-height
+@modal-title-line-height:     @line-height-base;
+
+//** Background color of modal content area
+@modal-content-bg:                             #fff;
+//** Modal content border color
+@modal-content-border-color:                   rgba(0,0,0,.2);
+//** Modal content border color **for IE8**
+@modal-content-fallback-border-color:          #999;
+
+//** Modal backdrop background color
+@modal-backdrop-bg:           #000;
+//** Modal backdrop opacity
+@modal-backdrop-opacity:      .5;
+//** Modal header border color
+@modal-header-border-color:   #e5e5e5;
+//** Modal footer border color
+@modal-footer-border-color:   @modal-header-border-color;
+
+@modal-lg:                    900px;
+@modal-md:                    600px;
+@modal-sm:                    300px;
+
+
+//== Alerts
+//
+//## Define alert colors, border radius, and padding.
+
+@alert-padding:               15px;
+@alert-border-radius:         @border-radius-base;
+@alert-link-font-weight:      bold;
+
+@alert-success-bg:            @state-success-bg;
+@alert-success-text:          @state-success-text;
+@alert-success-border:        @state-success-border;
+
+@alert-info-bg:               @state-info-bg;
+@alert-info-text:             @state-info-text;
+@alert-info-border:           @state-info-border;
+
+@alert-warning-bg:            @state-warning-bg;
+@alert-warning-text:          @state-warning-text;
+@alert-warning-border:        @state-warning-border;
+
+@alert-danger-bg:             @state-danger-bg;
+@alert-danger-text:           @state-danger-text;
+@alert-danger-border:         @state-danger-border;
+
+
+//== Progress bars
+//
+//##
+
+//** Background color of the whole progress component
+@progress-bg:                 #f5f5f5;
+//** Progress bar text color
+@progress-bar-color:          #fff;
+//** Variable for setting rounded corners on progress bar.
+@progress-border-radius:      @border-radius-base;
+
+//** Default progress bar color
+@progress-bar-bg:             @brand-primary;
+//** Success progress bar color
+@progress-bar-success-bg:     @brand-success;
+//** Warning progress bar color
+@progress-bar-warning-bg:     @brand-warning;
+//** Danger progress bar color
+@progress-bar-danger-bg:      @brand-danger;
+//** Info progress bar color
+@progress-bar-info-bg:        @brand-info;
+
+
+//== List group
+//
+//##
+
+//** Background color on `.list-group-item`
+@list-group-bg:                 #fff;
+//** `.list-group-item` border color
+@list-group-border:             #ddd;
+//** List group border radius
+@list-group-border-radius:      @border-radius-base;
+
+//** Background color of single list items on hover
+@list-group-hover-bg:           #f5f5f5;
+//** Text color of active list items
+@list-group-active-color:       @component-active-color;
+//** Background color of active list items
+@list-group-active-bg:          @component-active-bg;
+//** Border color of active list elements
+@list-group-active-border:      @list-group-active-bg;
+//** Text color for content within active list items
+@list-group-active-text-color:  lighten(@list-group-active-bg, 40%);
+
+//** Text color of disabled list items
+@list-group-disabled-color:      @gray-light;
+//** Background color of disabled list items
+@list-group-disabled-bg:         @gray-lighter;
+//** Text color for content within disabled list items
+@list-group-disabled-text-color: @list-group-disabled-color;
+
+@list-group-link-color:         #555;
+@list-group-link-hover-color:   @list-group-link-color;
+@list-group-link-heading-color: #333;
+
+
+//== Panels
+//
+//##
+
+@panel-bg:                    #fff;
+@panel-body-padding:          15px;
+@panel-heading-padding:       10px 15px;
+@panel-footer-padding:        @panel-heading-padding;
+@panel-border-radius:         @border-radius-base;
+
+//** Border color for elements within panels
+@panel-inner-border:          #ddd;
+@panel-footer-bg:             #f5f5f5;
+
+@panel-default-text:          @gray-dark;
+@panel-default-border:        #ddd;
+@panel-default-heading-bg:    #f5f5f5;
+
+@panel-primary-text:          #fff;
+@panel-primary-border:        @brand-primary;
+@panel-primary-heading-bg:    @brand-primary;
+
+@panel-success-text:          @state-success-text;
+@panel-success-border:        @state-success-border;
+@panel-success-heading-bg:    @state-success-bg;
+
+@panel-info-text:             @state-info-text;
+@panel-info-border:           @state-info-border;
+@panel-info-heading-bg:       @state-info-bg;
+
+@panel-warning-text:          @state-warning-text;
+@panel-warning-border:        @state-warning-border;
+@panel-warning-heading-bg:    @state-warning-bg;
+
+@panel-danger-text:           @state-danger-text;
+@panel-danger-border:         @state-danger-border;
+@panel-danger-heading-bg:     @state-danger-bg;
+
+
+//== Thumbnails
+//
+//##
+
+//** Padding around the thumbnail image
+@thumbnail-padding:           4px;
+//** Thumbnail background color
+@thumbnail-bg:                @body-bg;
+//** Thumbnail border color
+@thumbnail-border:            #ddd;
+//** Thumbnail border radius
+@thumbnail-border-radius:     @border-radius-base;
+
+//** Custom text color for thumbnail captions
+@thumbnail-caption-color:     @text-color;
+//** Padding around the thumbnail caption
+@thumbnail-caption-padding:   9px;
+
+
+//== Wells
+//
+//##
+
+@well-bg:                     #f5f5f5;
+@well-border:                 darken(@well-bg, 7%);
+
+
+//== Badges
+//
+//##
+
+@badge-color:                 #fff;
+//** Linked badge text color on hover
+@badge-link-hover-color:      #fff;
+@badge-bg:                    @gray-light;
+
+//** Badge text color in active nav link
+@badge-active-color:          @link-color;
+//** Badge background color in active nav link
+@badge-active-bg:             #fff;
+
+@badge-font-weight:           bold;
+@badge-line-height:           1;
+@badge-border-radius:         10px;
+
+
+//== Breadcrumbs
+//
+//##
+
+@breadcrumb-padding-vertical:   8px;
+@breadcrumb-padding-horizontal: 15px;
+//** Breadcrumb background color
+@breadcrumb-bg:                 #f5f5f5;
+//** Breadcrumb text color
+@breadcrumb-color:              #ccc;
+//** Text color of current page in the breadcrumb
+@breadcrumb-active-color:       @gray-light;
+//** Textual separator for between breadcrumb elements
+@breadcrumb-separator:          "/";
+
+
+//== Carousel
+//
+//##
+
+@carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);
+
+@carousel-control-color:                      #fff;
+@carousel-control-width:                      15%;
+@carousel-control-opacity:                    .5;
+@carousel-control-font-size:                  20px;
+
+@carousel-indicator-active-bg:                #fff;
+@carousel-indicator-border-color:             #fff;
+
+@carousel-caption-color:                      #fff;
+
+
+//== Close
+//
+//##
+
+@close-font-weight:           bold;
+@close-color:                 #000;
+@close-text-shadow:           0 1px 0 #fff;
+
+
+//== Code
+//
+//##
+
+@code-color:                  #c7254e;
+@code-bg:                     #f9f2f4;
+
+@kbd-color:                   #fff;
+@kbd-bg:                      #333;
+
+@pre-bg:                      #f5f5f5;
+@pre-color:                   @gray-dark;
+@pre-border-color:            #ccc;
+@pre-scrollable-max-height:   340px;
+
+
+//== Type
+//
+//##
+
+//** Horizontal offset for forms and lists.
+@component-offset-horizontal: 180px;
+//** Text muted color
+@text-muted:                  @gray-light;
+//** Abbreviations and acronyms border color
+@abbr-border-color:           @gray-light;
+//** Headings small color
+@headings-small-color:        @gray-light;
+//** Blockquote small color
+@blockquote-small-color:      @gray-light;
+//** Blockquote font size
+@blockquote-font-size:        (@font-size-base * 1.25);
+//** Blockquote border color
+@blockquote-border-color:     @gray-lighter;
+//** Page header border color
+@page-header-border-color:    @gray-lighter;
+//** Width of horizontal description list titles
+@dl-horizontal-offset:        @component-offset-horizontal;
+//** Point at which .dl-horizontal becomes horizontal
+@dl-horizontal-breakpoint:    @grid-float-breakpoint;
+//** Horizontal line color.
+@hr-border:                   @gray-lighter;
diff --git a/frontend/css/vendors/less/wells.less b/frontend/css/vendors/less/wells.less
new file mode 100644
index 0000000..15d072b
--- /dev/null
+++ b/frontend/css/vendors/less/wells.less
@@ -0,0 +1,29 @@
+//
+// Wells
+// --------------------------------------------------
+
+
+// Base class
+.well {
+  min-height: 20px;
+  padding: 19px;
+  margin-bottom: 20px;
+  background-color: @well-bg;
+  border: 1px solid @well-border;
+  border-radius: @border-radius-base;
+  .box-shadow(inset 0 1px 1px rgba(0,0,0,.05));
+  blockquote {
+    border-color: #ddd;
+    border-color: rgba(0,0,0,.15);
+  }
+}
+
+// Sizes
+.well-lg {
+  padding: 24px;
+  border-radius: @border-radius-large;
+}
+.well-sm {
+  padding: 9px;
+  border-radius: @border-radius-small;
+}
diff --git a/frontend/css/vendors/ng-tags-input.css b/frontend/css/vendors/ng-tags-input.css
new file mode 100644
index 0000000..795da15
--- /dev/null
+++ b/frontend/css/vendors/ng-tags-input.css
@@ -0,0 +1,129 @@
+tags-input *, *:before, *:after {
+  -moz-box-sizing: border-box;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+}
+
+tags-input .host {
+  position: relative;
+  margin-top: 5px;
+  margin-bottom: 5px;
+}
+tags-input .host:active {
+  outline: none;
+}
+
+tags-input .tags {
+  -moz-appearance: textfield;
+  -webkit-appearance: textfield;
+  padding: 1px;
+  overflow: hidden;
+  word-wrap: break-word;
+  cursor: text;
+  background-color: white;
+  border: 1px solid darkgray;
+  box-shadow: 1px 1px 1px 0 lightgray inset;
+}
+tags-input .tags.focused {
+  outline: none;
+  -webkit-box-shadow: 0 0 3px 1px rgba(5, 139, 242, 0.6);
+  -moz-box-shadow: 0 0 3px 1px rgba(5, 139, 242, 0.6);
+  box-shadow: 0 0 3px 1px rgba(5, 139, 242, 0.6);
+}
+tags-input .tags .tag-list {
+  margin: 0;
+  padding: 0;
+  list-style-type: none;
+}
+tags-input .tags .tag-item {
+  margin: 2px;
+  padding: 0 5px;
+  display: inline-block;
+  float: left;
+  font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
+  height: 26px;
+  line-height: 25px;
+  border: 1px solid #acacac;
+  border-radius: 3px;
+  background: -webkit-linear-gradient(top, #f0f9ff 0%, #cbebff 47%, #a1dbff 100%);
+  background: linear-gradient(to bottom, #f0f9ff 0%, #cbebff 47%, #a1dbff 100%);
+}
+tags-input .tags .tag-item.selected {
+  background: -webkit-linear-gradient(top, #febbbb 0%, #fe9090 45%, #ff5c5c 100%);
+  background: linear-gradient(to bottom, #febbbb 0%, #fe9090 45%, #ff5c5c 100%);
+}
+tags-input .tags .tag-item .remove-button {
+  margin: 0 0 0 5px;
+  padding: 0;
+  border: none;
+  background: none;
+  cursor: pointer;
+  vertical-align: middle;
+  font: bold 16px Arial, sans-serif;
+  color: #585858;
+}
+tags-input .tags .tag-item .remove-button:active {
+  color: red;
+}
+tags-input .tags .input {
+  border: 0;
+  outline: none;
+  margin: 2px;
+  padding: 0;
+  padding-left: 5px;
+  float: left;
+  height: 26px;
+  font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
+}
+tags-input .tags .input.invalid-tag {
+  color: red;
+}
+tags-input .tags .input::-ms-clear {
+  display: none;
+}
+tags-input.ng-invalid .tags {
+  -webkit-box-shadow: 0 0 3px 1px rgba(255, 0, 0, 0.6);
+  -moz-box-shadow: 0 0 3px 1px rgba(255, 0, 0, 0.6);
+  box-shadow: 0 0 3px 1px rgba(255, 0, 0, 0.6);
+}
+
+tags-input .autocomplete {
+  margin-top: 5px;
+  position: absolute;
+  padding: 5px 0;
+  z-index: 999;
+  width: 100%;
+  background-color: white;
+  border: 1px solid rgba(0, 0, 0, 0.2);
+  -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+  -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+}
+tags-input .autocomplete .suggestion-list {
+  margin: 0;
+  padding: 0;
+  list-style-type: none;
+}
+tags-input .autocomplete .suggestion-item {
+  padding: 5px 10px;
+  cursor: pointer;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  font: 16px "Helvetica Neue", Helvetica, Arial, sans-serif;
+  color: black;
+  background-color: white;
+}
+tags-input .autocomplete .suggestion-item.selected {
+  color: white;
+  background-color: #0097cf;
+}
+tags-input .autocomplete .suggestion-item.selected em {
+  color: white;
+  background-color: #0097cf;
+}
+tags-input .autocomplete .suggestion-item em {
+  font: normal bold 16px "Helvetica Neue", Helvetica, Arial, sans-serif;
+  color: black;
+  background-color: white;
+}
diff --git a/frontend/css/vendors/social_foundicons.css b/frontend/css/vendors/social_foundicons.css
new file mode 100755
index 0000000..914f647
--- /dev/null
+++ b/frontend/css/vendors/social_foundicons.css
@@ -0,0 +1,148 @@
+/* font-face */
+@font-face {
+  font-family: "SocialFoundicons";
+  src: url("../fonts/social_foundicons.eot");
+  src: url("../fonts/social_foundicons.eot?#iefix") format("embedded-opentype"), url("../fonts/social_foundicons.woff") format("woff"), url("../fonts/social_foundicons.ttf") format("truetype"), url("../fonts/social_foundicons.svg#SocialFoundicons") format("svg");
+  font-weight: normal;
+  font-style: normal;
+}
+
+/* global foundicon styles */
+[class*="foundicon-"] {
+  display: inline;
+  width: auto;
+  height: auto;
+  line-height: inherit;
+  vertical-align: baseline;
+  background-image: none;
+  background-position: 0 0;
+  background-repeat: repeat;
+}
+
+[class*="foundicon-"]:before {
+  font-family: "SocialFoundicons";
+  font-weight: normal;
+  font-style: normal;
+  text-decoration: inherit;
+}
+
+/* icons */
+.foundicon-thumb-up:before {
+  content: "\f000";
+}
+
+.foundicon-thumb-down:before {
+  content: "\f001";
+}
+
+.foundicon-rss:before {
+  content: "\f002";
+}
+
+.foundicon-facebook:before {
+  content: "\f003";
+}
+
+.foundicon-twitter:before {
+  content: "\f004";
+}
+
+.foundicon-pinterest:before {
+  content: "\f005";
+}
+
+.foundicon-github:before {
+  content: "\f006";
+}
+
+.foundicon-path:before {
+  content: "\f007";
+}
+
+.foundicon-linkedin:before {
+  content: "\f008";
+}
+
+.foundicon-dribbble:before {
+  content: "\f009";
+}
+
+.foundicon-stumble-upon:before {
+  content: "\f00a";
+}
+
+.foundicon-behance:before {
+  content: "\f00b";
+}
+
+.foundicon-reddit:before {
+  content: "\f00c";
+}
+
+.foundicon-google-plus:before {
+  content: "\f00d";
+}
+
+.foundicon-youtube:before {
+  content: "\f00e";
+}
+
+.foundicon-vimeo:before {
+  content: "\f00f";
+}
+
+.foundicon-flickr:before {
+  content: "\f010";
+}
+
+.foundicon-slideshare:before {
+  content: "\f011";
+}
+
+.foundicon-picassa:before {
+  content: "\f012";
+}
+
+.foundicon-skype:before {
+  content: "\f013";
+}
+
+.foundicon-steam:before {
+  content: "\f014";
+}
+
+.foundicon-instagram:before {
+  content: "\f015";
+}
+
+.foundicon-foursquare:before {
+  content: "\f016";
+}
+
+.foundicon-delicious:before {
+  content: "\f017";
+}
+
+.foundicon-chat:before {
+  content: "\f018";
+}
+
+.foundicon-torso:before {
+  content: "\f019";
+}
+
+.foundicon-tumblr:before {
+  content: "\f01a";
+}
+
+.foundicon-video-chat:before {
+  content: "\f01b";
+}
+
+.foundicon-digg:before {
+  content: "\f01c";
+}
+
+.foundicon-wordpress:before {
+  content: "\f01d";
+}
diff --git a/frontend/karma.conf.js b/frontend/karma.conf.js
new file mode 100644
index 0000000..cff5782
--- /dev/null
+++ b/frontend/karma.conf.js
@@ -0,0 +1,33 @@
+module.exports = function(config){
+  config.set({
+
+    basePath : './',
+
+    files : [
+      'bower_components/angular/angular.js',
+      'bower_components/angular-route/angular-route.js',
+      'bower_components/angular-mocks/angular-mocks.js',
+      'bower_components/angular-scenario/angular-scenario.js',
+      'src/**/*.js'
+    ],
+
+    autoWatch : true,
+
+    frameworks: ['jasmine'],
+
+    browsers : ['Chrome'],
+
+    plugins : [
+      'karma-chrome-launcher',
+      'karma-firefox-launcher',
+      'karma-jasmine',
+      'karma-junit-reporter'
+    ],
+
+    junitReporter : {
+      outputFile: 'test_out/unit.xml',
+      suite: 'unit'
+    }
+
+  });
+};
\ No newline at end of file
diff --git a/frontend/locations.ini b/frontend/locations.ini
new file mode 100644
index 0000000..0f90db6
--- /dev/null
+++ b/frontend/locations.ini
@@ -0,0 +1,2 @@
+ae_statics_location = ../backend/src/appenlight/static
+ae_webassets_location = ../webassets
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..7355593
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,25 @@
+{
+  "name": "errormator",
+  "description": "JS layer for Errormator",
+  "devDependencies": {
+    "bower": "",
+    "bower-requirejs": "",
+    "grunt": "",
+    "grunt-angular-templates": "~0.5.7",
+    "grunt-bower-concat": "~0.3.0",
+    "grunt-bower-requirejs": "~1.0.0",
+    "grunt-contrib-concat": "~0.5.0",
+    "grunt-contrib-copy": "~0.5.0",
+    "grunt-contrib-jshint": "",
+    "grunt-contrib-less": "~0.11.4",
+    "grunt-contrib-nodeunit": "",
+    "grunt-contrib-requirejs": "~0.4.4",
+    "grunt-contrib-uglify": "",
+    "grunt-contrib-watch": "",
+    "grunt-remove-logging": "~0.2.0",
+    "karma": "^0.12.37",
+    "underscore": "~1.6.0",
+    "yo": "",
+    "ini": ""
+  }
+}
diff --git a/frontend/readme.rst b/frontend/readme.rst
new file mode 100644
index 0000000..ca8c4c2
--- /dev/null
+++ b/frontend/readme.rst
@@ -0,0 +1,45 @@
+Javascript frontend for App Enlight
+===================================
+
+To fetch all the requirememts you need to have nodejs and npm installed on your dev machine, then from this dir execute::
+
+    npm install
+    npm install -g bower
+    npm install -g grunt-cli
+    bower install
+
+This will fetch all the required components to build front with grunt.
+
+
+To build production version (builds both js and css) just run::
+
+    grunt
+
+To work on dev code version (builds js with comments and css) run:
+
+    grunt watch
+
+You generally shouldn't need to run those separately but still:
+
+To work on just Javascript version with comments run:
+
+    grunt watch:dev
+
+To work on just CSS files run:
+
+    grunt watch:css
+
+Ubuntu/Debian and broken node - running from node_modules instead system ones
+-----------------------------------------------------------------------------
+
+Download this:
+
+    http://nodejs.org/dist/v0.10.32/node-v0.10.32-linux-x64.tar.gz
+
+unpack to your home into "node" directory then edit your .bashrc file to include:
+
+export PATH=$PATH:~/node/bin
+
+now you will be able to execute all the comands above just fine
+
+
diff --git a/frontend/src/app.js b/frontend/src/app.js
new file mode 100644
index 0000000..c235ce6
--- /dev/null
+++ b/frontend/src/app.js
@@ -0,0 +1,187 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+'use strict';
+
+// Declare app level module which depends on filters, and services
+angular.module('appenlight.base', [
+    'ngRoute',
+    'ui.router',
+    'ui.router.router',
+    'underscore',
+    'ui.bootstrap',
+    'ngResource',
+    'ngAnimate',
+    'ngCookies',
+    'smart-table',
+    'angular-toArrayFilter',
+    'mentio'
+]);
+
+angular.module('appenlight.filters', []);
+angular.module('appenlight.templates', []);
+angular.module('appenlight.controllers', ['appenlight.base']);
+angular.module('appenlight.directives', [
+    'appenlight.directives.appVersion',
+    'appenlight.directives.c3chart',
+    'appenlight.directives.confirmValidate',
+    'appenlight.directives.focus',
+    'appenlight.directives.formErrors',
+    'appenlight.directives.humanFormat',
+    'appenlight.directives.isoToRelativeTime',
+    'appenlight.directives.permissionsForm',
+    'appenlight.directives.smallReportGroupList',
+    'appenlight.directives.smallReportList',
+    'appenlight.directives.pluginConfig',
+    'appenlight.directives.recursive',
+    'appenlight.directives.reportAlertAction',
+    'appenlight.directives.postProcessAction',
+    'appenlight.directives.rule',
+    'appenlight.directives.ruleReadOnly'
+]);
+angular.module('appenlight.services', [
+    'appenlight.services.chartResultParser',
+    'appenlight.services.resources',
+    'appenlight.services.stateHolder',
+    'appenlight.services.typeAheadTagHelper',
+    'appenlight.services.UUIDProvider'
+]).value('version', '0.1');
+
+
+var pluginsToLoad = _.map(decodeEncodedJSON(window.AE.plugins),
+    function(item){
+        return item.config.angular_module
+    });
+console.log(pluginsToLoad);
+angular.module('appenlight.plugins', pluginsToLoad);
+
+var app = angular.module('appenlight', [
+    'appenlight.base',
+    'appenlight.config',
+    'appenlight.user',
+    'appenlight.templates',
+    'appenlight.filters',
+    'appenlight.services',
+    'appenlight.directives',
+    'appenlight.controllers',
+    'appenlight.plugins'
+]);
+
+function kickstartAE() {
+
+
+
+    app.config(['$httpProvider', '$uibTooltipProvider', '$locationProvider', function ($httpProvider, $uibTooltipProvider, $locationProvider) {
+        $locationProvider.html5Mode(true);
+        $httpProvider.interceptors.push(['$q', '$rootScope', '$timeout', 'stateHolder', function ($q, $rootScope, $timeout, stateHolder) {
+            return {
+                'response': function (response) {
+                    var flashMessages = angular.fromJson(response.headers('x-flash-messages'));
+                    if (flashMessages && flashMessages.length > 0) {
+                        stateHolder.flashMessages.extend(flashMessages);
+                    }
+                    return response;
+                },
+                'responseError': function (rejection) {
+                    console.log(rejection);
+                    if (rejection.status > 299 && rejection.status !== 422) {
+                        stateHolder.flashMessages.extend([{
+                            msg: 'Response status code: ' + rejection.status + ', "' + rejection.statusText + '", url: ' + rejection.config.url,
+                            type: 'error'
+                        }]);
+                    }
+                    if (rejection.status == 0) {
+                        stateHolder.flashMessages.extend([{
+                            msg: 'Response timeout',
+                            type: 'error'
+                        }]);
+                    }
+                    var flashMessages = angular.fromJson(rejection.headers('x-flash-messages'));
+                    if (flashMessages && flashMessages.length > 0) {
+                        stateHolder.flashMessages.extend(flashMessages);
+                    }
+
+                    return $q.reject(rejection);
+                }
+            }
+        }]);
+
+        $uibTooltipProvider.options({appendToBody: true});
+
+    }]);
+
+
+    app.config(function ($provide) {
+        $provide.decorator("$exceptionHandler", function ($delegate) {
+            return function (exception, cause) {
+                $delegate(exception, cause);
+                if (typeof AppEnlight !== 'undefined') {
+                    AppEnlight.grabError(exception);
+                }
+            };
+        });
+    });
+
+    app.run(['$rootScope', '$timeout', 'stateHolder', '$state', '$location', '$transitions', '$window', 'AeConfig', 'AeUser',
+        function ($rootScope, $timeout, stateHolder, $state, $location, $transitions, $window, AeConfig, AeUser) {
+            $rootScope.$state = $state;
+            $rootScope.stateHolder = stateHolder;
+            $rootScope.flash = stateHolder.flashMessages.list;
+            $rootScope.closeAlert = stateHolder.flashMessages.closeAlert;
+            $rootScope.AeConfig = AeConfig;
+            $rootScope.AeUser = AeUser;
+
+            var transitionApp = function($transition$, $state) {
+                // redirect user to /register unless its one of open views
+                var isGuestState = [
+                        'report.view_detail',
+                        'report.view_group',
+                        'dashboard.view'
+                    ].indexOf($transition$.to().name) !== -1;
+
+                var path = $window.location.pathname;
+                // strip trailing slash
+                if (path.substr(path.length - 1) === '/') {
+                    path = path.substr(0, path.length - 1);
+                }
+                var isOpenView = false;
+                var openViews = [
+                    AeConfig.urls.otherRoutes.lostPassword,
+                    AeConfig.urls.otherRoutes.lostPasswordGenerate
+                ];
+                console.log('$transitions.onBefore', path, $transition$.to().name, $state);
+                _.each(openViews, function (url) {
+                    var url = '/' + url.split('/').slice(3).join('/');
+                    if (url === path) {
+                        isOpenView = true;
+                    }
+                });
+                if (AeUser.id === null && !isGuestState && !isOpenView) {
+                    if (window.location.toString().indexOf(AeConfig.urls.otherRoutes.register) === -1) {
+                        console.log('redirect to register');
+                        $window.location = AeConfig.urls.otherRoutes.register + '?came_from=' + encodeURIComponent(window.location);
+                        return false;
+                    }
+                    return false;
+                }
+            };
+            $transitions.onBefore({}, transitionApp);
+
+        }]);
+}
diff --git a/frontend/src/config.js b/frontend/src/config.js
new file mode 100644
index 0000000..df3e2f9
--- /dev/null
+++ b/frontend/src/config.js
@@ -0,0 +1,35 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+var aeconfig = angular.module('appenlight.config', []);
+aeconfig.factory('AeConfig', function () {
+    var obj = {};
+    obj.flashMessages = decodeEncodedJSON(window.AE.flash_messages);
+    obj.timeOptions = decodeEncodedJSON(window.AE.timeOptions);
+    obj.plugins = decodeEncodedJSON(window.AE.plugins);
+    obj.ws_url = window.AE.ws_url;
+    obj.urls = window.AE.urls;
+
+    // set keys on values because we wont be able to retrieve them everywhere
+    for (var key in obj.timeOptions) {
+        obj.timeOptions[key]['key'] = key;
+    }
+    console.info('config', obj);
+    return obj;
+});
diff --git a/frontend/src/controllers/admin/applications/applications_list.js b/frontend/src/controllers/admin/applications/applications_list.js
new file mode 100644
index 0000000..9942bc9
--- /dev/null
+++ b/frontend/src/controllers/admin/applications/applications_list.js
@@ -0,0 +1,35 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers').controller('AdminApplicationsListController', AdminApplicationsListController);
+
+AdminApplicationsListController.$inject = ['applicationsResource'];
+
+function AdminApplicationsListController(applicationsResource) {
+    console.debug('AdminApplicationsListController');
+    var vm = this;
+    vm.loading = {applications: true};
+
+    vm.applications = applicationsResource.query({
+        root_list: true,
+        resource_type: 'application'
+    }, function (data) {
+        vm.loading = {applications: false};
+    });
+};
diff --git a/frontend/src/controllers/admin/config.js b/frontend/src/controllers/admin/config.js
new file mode 100644
index 0000000..9add726
--- /dev/null
+++ b/frontend/src/controllers/admin/config.js
@@ -0,0 +1,57 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers').controller('ConfigsListController', ConfigsListController);
+
+ConfigsListController.$inject = ['configsResource', 'configsNoIdResource'];
+
+function ConfigsListController(configsResource, configsNoIdResource) {
+    var vm = this;
+    vm.loading = {config: true};
+
+    var filters = [
+        'template_footer_html:global',
+        'list_groups_to_non_admins:global',
+        'per_application_reports_rate_limit:global',
+        'per_application_logs_rate_limit:global',
+        'per_application_metrics_rate_limit:global',
+    ];
+
+    vm.configs = {};
+
+    vm.configList = configsResource.query({filter: filters},
+        function (data) {
+            vm.loading = {config: false};
+            _.each(data, function (item) {
+                if (vm.configs[item.section] === undefined) {
+                    vm.configs[item.section] = {};
+                }
+                vm.configs[item.section][item.key] = item;
+            });
+        });
+
+    vm.save = function () {
+        vm.loading.config = true;
+        _.each(vm.configList, function (item) {
+            item.$save();
+        });
+        vm.loading.config = false;
+    };
+
+};
diff --git a/frontend/src/controllers/admin/groups/groups_create.js b/frontend/src/controllers/admin/groups/groups_create.js
new file mode 100644
index 0000000..88de7f8
--- /dev/null
+++ b/frontend/src/controllers/admin/groups/groups_create.js
@@ -0,0 +1,142 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers').controller('AdminGroupsCreateController', AdminGroupsCreateController);
+
+AdminGroupsCreateController.$inject = ['$state', 'groupsResource', 'groupsPropertyResource', 'sectionViewResource', 'AeConfig'];
+
+function AdminGroupsCreateController($state, groupsResource, groupsPropertyResource, sectionViewResource, AeConfig) {
+    console.debug('AdminGroupsCreateController');
+    var vm = this;
+    vm.loading = {
+        group: false,
+        resource_permissions: false,
+        users: false
+    };
+
+    vm.form = {
+        autocompleteUser: '',
+    }
+
+
+    if (typeof $state.params.groupId !== 'undefined') {
+        vm.loading.group = true;
+        var groupId = $state.params.groupId;
+        vm.group = groupsResource.get({groupId: groupId}, function (data) {
+            vm.loading.group = false;
+        });
+
+        vm.resource_permissions = groupsPropertyResource.query(
+            {groupId: groupId, key: 'resource_permissions'}, function (data) {
+                vm.loading.resource_permissions = false;
+                var tmpObj = {
+                    'group': {
+                        'application': {},
+                        'dashboard': {}
+                    }
+                };
+                _.each(data, function (item) {
+                    console.log(item);
+                    var section = tmpObj[item.type][item.resource_type];
+                    if (typeof section[item.resource_id] == 'undefined') {
+                        section[item.resource_id] = {
+                            self: item,
+                            permissions: []
+                        }
+                    }
+                    section[item.resource_id].permissions.push(item.perm_name);
+
+                });
+                console.log(tmpObj)
+                vm.resourcePermissions = tmpObj;
+            });
+
+        vm.users = groupsPropertyResource.query(
+            {groupId: groupId, key: 'users'}, function (data) {
+                vm.loading.users = false;
+            }, function () {
+                vm.loading.users = false;
+            });
+
+    }
+    else {
+        var groupId = null;
+    }
+
+    var formResponse = function (response) {
+        if (response.status === 422) {
+            setServerValidation(vm.groupForm, response.data);
+        }
+        vm.loading.group = false;
+    };
+
+    vm.createGroup = function () {
+        vm.loading.group = true;
+        if (groupId) {
+            groupsResource.update({groupId: vm.group.id}, vm.group, function (data) {
+                setServerValidation(vm.groupForm);
+                vm.loading.group = false;
+            }, formResponse);
+        }
+        else {
+            groupsResource.save(vm.group, function (data) {
+                $state.go('admin.group.update', {groupId: data.id});
+            }, formResponse);
+        }
+    };
+
+    vm.removeUser = function (user) {
+        groupsPropertyResource.delete(
+            {groupId: groupId, key: 'users', user_name: user.user_name},
+            function (data) {
+                vm.loading.users = false;
+                vm.users = _.filter(vm.users, function (item) {
+                    return item != user;
+                });
+            }, function () {
+                vm.loading.users = false;
+            });
+    };
+
+    vm.addUser = function () {
+        groupsPropertyResource.save(
+            {groupId: groupId, key: 'users'},
+            {user_name: vm.form.autocompleteUser},
+            function (data) {
+                vm.loading.users = false;
+                vm.users.push(data);
+                vm.form.autocompleteUser = '';
+            }, function () {
+                vm.loading.users = false;
+            });
+    }
+
+    vm.searchUsers = function (searchPhrase) {
+        console.log(searchPhrase);
+        return sectionViewResource.query({
+            section: 'users_section',
+            view: 'search_users',
+            'user_name': searchPhrase
+        }).$promise.then(function (data) {
+                return _.map(data, function (item) {
+                    return item.user;
+                });
+            });
+    }
+};
diff --git a/frontend/src/controllers/admin/groups/groups_list.js b/frontend/src/controllers/admin/groups/groups_list.js
new file mode 100644
index 0000000..a741810
--- /dev/null
+++ b/frontend/src/controllers/admin/groups/groups_list.js
@@ -0,0 +1,53 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers').controller('AdminGroupsController', AdminGroupsController);
+
+AdminGroupsController.$inject = ['groupsResource'];
+
+function AdminGroupsController(groupsResource) {
+    console.debug('AdminGroupsController');
+    var vm = this;
+    vm.loading = {groups: true};
+
+    vm.groups = groupsResource.query({}, function (data) {
+        vm.loading = {groups: false};
+        vm.activeUsers = _.reduce(vm.groups, function(memo, val){
+            if (val.status == 1){
+                return memo + 1;
+            }
+            return memo;
+        }, 0);
+        console.log(vm.groups);
+    });
+
+
+    vm.removeGroup = function (group) {
+        groupsResource.remove({groupId: group.id}, function (data, responseHeaders) {
+            console.log('x',data, responseHeaders());
+            if (data) {
+                var index = vm.groups.indexOf(group);
+                if (index !== -1) {
+                    vm.groups.splice(index, 1);
+                    vm.activeGroups -= 1;
+                }
+            }
+        });
+    }
+};
diff --git a/frontend/src/controllers/admin/partitions.js b/frontend/src/controllers/admin/partitions.js
new file mode 100644
index 0000000..d223844
--- /dev/null
+++ b/frontend/src/controllers/admin/partitions.js
@@ -0,0 +1,124 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers').controller('AdminPartitionsController', AdminPartitionsController);
+
+AdminPartitionsController.$inject = ['sectionViewResource'];
+
+function AdminPartitionsController(sectionViewResource) {
+    var vm = this;
+    vm.permanentPartitions = [];
+    vm.dailyPartitions = [];
+    vm.loading = {partitions: true};
+    vm.dailyChecked = false;
+    vm.permChecked = false;
+    vm.dailyConfirm = '';
+    vm.permConfirm = '';
+
+
+    vm.loadPartitions = function (data) {
+        var permanentPartitions = vm.transformPartitionList(
+            data.permanent_partitions);
+        var dailyPartitions = vm.transformPartitionList(
+            data.daily_partitions);
+        vm.permanentPartitions = permanentPartitions;
+        vm.dailyPartitions = dailyPartitions;
+        vm.loading = {partitions: false};
+    };
+
+    vm.setCheckedList = function (scope) {
+        var toTest = null;
+        if (scope === 'dailyPartitions'){
+            toTest = 'dailyChecked';
+        }
+        else{
+            toTest = 'permChecked';
+        }
+
+        if (vm[toTest]) {
+            var val = true;
+        }
+        else {
+            var val = false;
+        }
+        console.log('scope', scope);
+        _.each(vm[scope], function (item) {
+            _.each(item[1].pg, function (index) {
+                index.checked = val;
+            });
+            _.each(item[1].elasticsearch, function (index) {
+                index.checked = val;
+            });
+        });
+    }
+
+
+    vm.transformPartitionList = function (inputList) {
+        var outputList = [];
+
+        _.each(inputList, function (item) {
+            var time = [item[0], {
+                elasticsearch: [],
+                pg: []
+            }]
+            _.each(item[1].pg, function (index) {
+                time[1].pg.push({name: index, checked: false})
+            });
+            _.each(item[1].elasticsearch, function (index) {
+                time[1].elasticsearch.push({
+                    name: index,
+                    checked: false
+                })
+            });
+            outputList.push(time);
+        });
+        return outputList;
+    };
+
+    sectionViewResource.get({section:'admin_section', view: 'partitions'},
+        vm.loadPartitions);
+
+    vm.partitionsDelete = function (partitionType) {
+        var es_indices = [];
+        var pg_indices = [];
+        _.each(vm[partitionType], function (item) {
+            _.each(item[1].pg, function (index) {
+                if (index.checked) {
+                    pg_indices.push(index.name)
+                }
+            });
+            _.each(item[1].elasticsearch, function (index) {
+                if (index.checked) {
+                    es_indices.push(index.name)
+                }
+            });
+        });
+        console.log(es_indices, pg_indices);
+
+        vm.loading = {partitions: true};
+        sectionViewResource.save({section:'admin_section',
+            view: 'partitions_remove'}, {
+            es_indices: es_indices,
+            pg_indices: pg_indices,
+            confirm: 'CONFIRM'
+        }, vm.loadPartitions);
+
+    }
+
+}
diff --git a/frontend/src/controllers/admin/system.js b/frontend/src/controllers/admin/system.js
new file mode 100644
index 0000000..d823dac
--- /dev/null
+++ b/frontend/src/controllers/admin/system.js
@@ -0,0 +1,44 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers').controller('AdminSystemController', AdminSystemController);
+
+AdminSystemController.$inject = ['sectionViewResource'];
+
+function AdminSystemController(sectionViewResource) {
+    var vm = this;
+    vm.tables = [];
+    vm.loading = {system: true};
+    sectionViewResource.get({
+        section: 'admin_section',
+        view: 'system'
+    }, null, function (data) {
+        vm.DBtables = data.db_tables;
+        vm.ESIndices = data.es_indices;
+        vm.queueStats = data.queue_stats;
+        vm.systemLoad = data.system_load;
+        vm.packages = data.packages;
+        vm.processInfo = data.process_info;
+        vm.disks = data.disks;
+        vm.memory = data.memory;
+        vm.selfInfo = data.self_info;
+
+        vm.loading.system = false;
+    });
+};
diff --git a/frontend/src/controllers/admin/users/users_create.js b/frontend/src/controllers/admin/users/users_create.js
new file mode 100644
index 0000000..a39178c
--- /dev/null
+++ b/frontend/src/controllers/admin/users/users_create.js
@@ -0,0 +1,121 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers').controller('AdminUsersCreateController', AdminUsersCreateController);
+
+AdminUsersCreateController.$inject = ['$state', 'usersResource', 'usersPropertyResource', 'sectionViewResource', 'AeConfig'];
+
+function AdminUsersCreateController($state, usersResource, usersPropertyResource, sectionViewResource, AeConfig) {
+    console.debug('AdminUsersCreateController');
+    var vm = this;
+    vm.loading = {user: false};
+
+
+    if (typeof $state.params.userId !== 'undefined') {
+        vm.loading.user = true;
+        var userId = $state.params.userId;
+        vm.user = usersResource.get({userId: userId}, function (data) {
+            vm.loading.user = false;
+            // cast to true for angular checkbox
+            if (vm.user.status === 1) {
+                vm.user.status = true;
+            }
+        });
+
+        vm.resource_permissions = usersPropertyResource.query(
+            {userId: userId, key: 'resource_permissions'}, function (data) {
+                vm.loading.resource_permissions = false;
+                var tmpObj = {
+                    'user': {
+                        'application': {},
+                        'dashboard': {}
+                    },
+                    'group': {
+                        'application': {},
+                        'dashboard': {}
+                    }
+                };
+                _.each(data, function (item) {
+                    console.log(item);
+                    var section = tmpObj[item.type][item.resource_type];
+                    if (typeof section[item.resource_id] == 'undefined'){
+                        section[item.resource_id] = {
+                            self:item,
+                            permissions: []
+                        }
+                    }
+                    section[item.resource_id].permissions.push(item.perm_name);
+
+                });
+                console.log(tmpObj)
+                vm.resourcePermissions = tmpObj;
+            });
+
+    }
+    else {
+        var userId = null;
+        vm.user = {
+            status: true
+        }
+    }
+
+    var formResponse = function (response) {
+        if (response.status == 422) {
+            setServerValidation(vm.profileForm, response.data);
+        }
+        vm.loading.user = false;
+    }
+
+    vm.createUser = function () {
+        vm.loading.user = true;
+        console.log('updateProfile');
+        if (userId) {
+            usersResource.update({userId: vm.user.id}, vm.user, function (data) {
+                setServerValidation(vm.profileForm);
+                vm.loading.user = false;
+            }, formResponse);
+        }
+        else {
+            usersResource.save(vm.user, function (data) {
+                $state.go('admin.user.update', {userId: data.id});
+            }, formResponse);
+        }
+    }
+
+    vm.generatePassword = function () {
+        var length = 8;
+        var charset = "abcdefghijklnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+        vm.gen_pass = "";
+        for (var i = 0, n = charset.length; i < length; ++i) {
+            vm.gen_pass += charset.charAt(Math.floor(Math.random() * n));
+        }
+        vm.user.user_password = '' + vm.gen_pass;
+        console.log('x', vm.gen_pass);
+    }
+
+    vm.reloginUser = function () {
+        sectionViewResource.get({
+            section: 'admin_section', view: 'relogin_user',
+            user_id: vm.user.id
+        }, function () {
+            window.location = AeConfig.urls.baseUrl;
+        });
+
+    }
+};
diff --git a/frontend/src/controllers/admin/users/users_list.js b/frontend/src/controllers/admin/users/users_list.js
new file mode 100644
index 0000000..b394362
--- /dev/null
+++ b/frontend/src/controllers/admin/users/users_list.js
@@ -0,0 +1,53 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers').controller('AdminUsersController', AdminUsersController);
+
+AdminUsersController.$inject = ['usersResource'];
+
+function AdminUsersController(usersResource) {
+    console.debug('AdminUsersController');
+    var vm = this;
+    vm.loading = {users: true};
+
+    vm.users = usersResource.query({}, function (data) {
+        vm.loading = {users: false};
+        vm.activeUsers = _.reduce(vm.users, function(memo, val){
+            if (val.status == 1){
+                return memo + 1;
+            }
+            return memo;
+        }, 0);
+        console.log(vm.users);
+    });
+
+
+    vm.removeUser = function (user) {
+        usersResource.remove({userId: user.id}, function (data, responseHeaders) {
+            console.log('x',data, responseHeaders());
+            if (data) {
+                var index = vm.users.indexOf(user);
+                if (index !== -1) {
+                    vm.users.splice(index, 1);
+                    vm.activeUsers -= 1;
+                }
+            }
+        });
+    }
+};
diff --git a/frontend/src/controllers/applications/applications_create.js b/frontend/src/controllers/applications/applications_create.js
new file mode 100644
index 0000000..b854baf
--- /dev/null
+++ b/frontend/src/controllers/applications/applications_create.js
@@ -0,0 +1,174 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers')
+    .controller('ApplicationsUpdateController', ApplicationsUpdateController)
+
+ApplicationsUpdateController.$inject = ['$state', 'applicationsNoIdResource', 'applicationsResource', 'applicationsPropertyResource', 'AeUser'];
+
+function ApplicationsUpdateController($state, applicationsNoIdResource, applicationsResource, applicationsPropertyResource, AeUser) {
+    'use strict';
+    console.debug('ApplicationsUpdateController');
+    var vm = this;
+    vm.loading = {application: false};
+
+    vm.groupingOptions = [
+        ['url_type', 'Error Type + location'],
+        ['url_traceback', 'Traceback + location'],
+        ['traceback_server', 'Traceback + Server'],
+    ];
+
+    var resourceId = $state.params.resourceId;
+
+
+    var options = {};
+
+    vm.momentJs = moment;
+    
+    vm.formTransferModel = {password:''};
+
+    // set initial data
+
+    if (resourceId === 'new') {
+        vm.resource = {
+            resource_id: null,
+            slow_report_threshold: 10,
+            error_report_threshold: 10,
+            allow_permanent_storage: true,
+            default_grouping: vm.groupingOptions[1][0]
+        };
+    }
+    else {
+        vm.loading.application = true;
+        vm.resource = applicationsResource.get({
+            'resourceId': resourceId
+        }, function (data) {
+            vm.loading.application = false;
+        });
+    }
+
+
+    vm.updateBasicForm = function () {
+        vm.loading.application = true;
+        if (vm.resource.resource_id === null) {
+            applicationsNoIdResource.save(null, vm.resource, function (data) {
+                console.log('apps',AeUser.applications);
+                AeUser.addApplication(data);
+                console.log('apps',AeUser.applications);
+                $state.go('applications.update', {resourceId: data.resource_id});
+                setServerValidation(vm.BasicForm);
+            }, function (response) {
+                if (response.status == 422) {
+                    setServerValidation(vm.BasicForm, response.data);
+                }
+                vm.loading.application = false;
+                console.log(vm.BasicForm);
+            });
+        }
+        else {
+            applicationsResource.update({resourceId: vm.resource.resource_id},
+                vm.resource, function (data) {
+                    vm.resource = data;
+                    vm.loading.application = false;
+                    setServerValidation(vm.BasicForm);
+                }, function (response) {
+                    if (response.status == 422) {
+                        setServerValidation(vm.BasicForm, response.data);
+                    }
+                    vm.loading.application = false;
+                });
+        }
+    };
+
+    vm.addRule = function () {
+        console.log('addrule');
+        applicationsPropertyResource.save({
+                resourceId: vm.resource.resource_id,
+                key: 'postprocessing_rules'
+            }, null,
+            function (data) {
+                vm.resource.postprocessing_rules.push(data);
+            }
+        );
+    };
+
+    vm.regenerateAPIKeys = function(){
+        vm.loading.application = true;
+        applicationsPropertyResource.save({
+                resourceId: vm.resource.resource_id,
+                key: 'api_key'
+            }, {password: vm.regenerateAPIKeysPassword},
+            function (data) {
+                vm.resource = data;
+                vm.loading.application = false;
+                vm.regenerateAPIKeysPassword = '';
+                setServerValidation(vm.regenerateAPIKeysForm);
+            },
+            function (response) {
+                if (response.status == 422) {
+                    setServerValidation(vm.regenerateAPIKeysForm, response.data);
+                    console.log(response.data);
+                }
+                vm.loading.application = false;
+            }
+        )
+    };
+
+    vm.deleteApplication = function(){
+        vm.loading.application = true;
+        applicationsPropertyResource.update({
+                resourceId: vm.resource.resource_id,
+                key: 'delete_resource'
+            }, vm.formDeleteModel,
+            function (data) {
+                console.log('apps',AeUser.applications);
+                AeUser.removeApplicationById(vm.resource.resource_id);
+                console.log('apps',AeUser.applications);
+                $state.go('applications.list');
+            },
+            function (response) {
+                if (response.status == 422) {
+                    setServerValidation(vm.formDelete, response.data);
+                    console.log(response.data);
+                }
+                vm.loading.application = false;
+            }
+        );
+    };
+
+    vm.transferApplication = function(){
+        vm.loading.application = true;
+        applicationsPropertyResource.update({
+                resourceId: vm.resource.resource_id,
+                key: 'owner'
+            }, vm.formTransferModel,
+            function (data) {
+                $state.go('applications.list');
+            },
+            function (response) {
+                if (response.status == 422) {
+                    setServerValidation(vm.formTransfer, response.data);
+                    console.log(response.data);
+                }
+                vm.loading.application = false;
+            }
+        )
+    }
+
+}
diff --git a/frontend/src/controllers/applications/integration.js b/frontend/src/controllers/applications/integration.js
new file mode 100644
index 0000000..a6368c4
--- /dev/null
+++ b/frontend/src/controllers/applications/integration.js
@@ -0,0 +1,87 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers')
+    .controller('IntegrationController', IntegrationController)
+
+IntegrationController.$inject = ['$state', 'integrationResource'];
+
+function IntegrationController($state, integrationResource) {
+    console.debug('IntegrationController');
+    var vm = this;
+    vm.loading = {integration: true};
+    vm.config = integrationResource.get(
+        {
+            integration: $state.params.integration,
+            action: 'setup',
+            resourceId: $state.params.resourceId
+        }, function (data) {
+            vm.loading.integration = false;
+        });
+
+    vm.configureIntegration = function () {
+        console.info('configureIntegration');
+        vm.loading.integration = true;
+        integrationResource.save(
+            {
+                integration: $state.params.integration,
+                action: 'setup',
+                resourceId: $state.params.resourceId
+            }, vm.config, function (data) {
+                vm.loading.integration = false;
+                setServerValidation(vm.integrationForm);
+            }, function (response) {
+                if (response.status == 422) {
+                    setServerValidation(vm.integrationForm, response.data);
+                }
+                vm.loading.integration = false;
+            });
+    };
+
+    vm.removeIntegration = function () {
+        console.info('removeIntegration');
+        integrationResource.remove({
+                integration: $state.params.integration,
+                resourceId: $state.params.resourceId,
+                action: 'delete'
+            },
+            function () {
+                $state.go('applications.integrations',
+                    {resourceId: $state.params.resourceId});
+            }
+        );
+    }
+
+    vm.testIntegration = function(to_test){
+        console.info('testIntegration', to_test);
+        vm.loading.integration = true;
+        integrationResource.save(
+            {
+                integration: $state.params.integration,
+                action: 'test_'+ to_test,
+                resourceId: $state.params.resourceId
+            }, vm.config, function (data) {
+                vm.loading.integration = false;
+            }, function (response) {
+                vm.loading.integration = false;
+            });
+    }
+
+    console.log(vm);
+}
diff --git a/frontend/src/controllers/applications/integrations.js b/frontend/src/controllers/applications/integrations.js
new file mode 100644
index 0000000..c33feaa
--- /dev/null
+++ b/frontend/src/controllers/applications/integrations.js
@@ -0,0 +1,33 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers')
+    .controller('IntegrationsListController', IntegrationsListController)
+
+IntegrationsListController.$inject = ['$state', 'applicationsResource'];
+
+function IntegrationsListController($state, applicationsResource) {
+    console.debug('IntegrationsListController');
+    var vm = this;
+    vm.loading = {application: true};
+    vm.resource = applicationsResource.get({resourceId: $state.params.resourceId}, function (data) {
+        vm.loading.application = false;
+        $state.current.data.resource = vm.resource;
+    });
+}
diff --git a/frontend/src/controllers/applications/list.js b/frontend/src/controllers/applications/list.js
new file mode 100644
index 0000000..007fb41
--- /dev/null
+++ b/frontend/src/controllers/applications/list.js
@@ -0,0 +1,32 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers')
+    .controller('ApplicationsListController', ApplicationsListController)
+
+ApplicationsListController.$inject = ['applicationsResource'];
+
+function ApplicationsListController(applicationsResource) {
+    console.debug('ApplicationsListController');
+    var vm = this;
+    vm.loading = {applications: true};
+    vm.applications = applicationsResource.query(null, function(){
+        vm.loading.applications = false;
+    });
+}
diff --git a/frontend/src/controllers/applications/purge_logs.js b/frontend/src/controllers/applications/purge_logs.js
new file mode 100644
index 0000000..fc44b99
--- /dev/null
+++ b/frontend/src/controllers/applications/purge_logs.js
@@ -0,0 +1,60 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers')
+    .controller('ApplicationsPurgeLogsController', ApplicationsPurgeLogsController)
+
+ApplicationsPurgeLogsController.$inject = ['applicationsResource', 'sectionViewResource', 'logsNoIdResource'];
+
+function ApplicationsPurgeLogsController(applicationsResource, sectionViewResource, logsNoIdResource) {
+    console.debug('ApplicationsPurgeLogsController');
+    var vm = this;
+    vm.loading = {applications: true};
+
+    vm.namespace = null;
+    vm.selectedResource = null;
+    vm.commonNamespaces = [];
+
+    vm.applications = applicationsResource.query({'type':'update_reports'}, function () {
+        vm.loading.applications = false;
+        vm.selectedResource = vm.applications[0].resource_id;
+        vm.getCommonKeys();
+    });
+
+    /**
+     * Fetches most commonly used tags in logs
+     */
+    vm.getCommonKeys = function () {
+        sectionViewResource.get({
+            section: 'logs_section',
+            view: 'common_tags',
+            resource: vm.selectedResource
+        }, function (data) {
+            vm.commonNamespaces = data['namespaces']
+        });
+    };
+
+    vm.purgeLogs = function () {
+        vm.loading.applications = true;
+        logsNoIdResource.delete({resource:vm.selectedResource,
+            namespace: vm.namespace}, function(){
+            vm.loading.applications = false;
+        });
+    }
+}
diff --git a/frontend/src/controllers/events.js b/frontend/src/controllers/events.js
new file mode 100644
index 0000000..9ab3007
--- /dev/null
+++ b/frontend/src/controllers/events.js
@@ -0,0 +1,44 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers').controller('EventsController', EventsController);
+
+EventsController.$inject = ['eventsNoIdResource', 'eventsResource'];
+
+function EventsController(eventsNoIdResource, eventsResource) {
+    console.info('EventsController');
+    var vm = this;
+
+    vm.loading = {events: true};
+
+    vm.events = eventsNoIdResource.query(
+        {key: 'events'},
+        function (data) {
+            vm.loading.events = false;
+        });
+
+
+    vm.closeEvent = function (event) {
+        console.log('closeEvent');
+        eventsResource.update({eventId: event.id}, {status: 0}, function (data) {
+            event.status = 0;
+        });
+    }
+
+}
diff --git a/frontend/src/controllers/front_dashboard.js b/frontend/src/controllers/front_dashboard.js
new file mode 100644
index 0000000..a5b73c2
--- /dev/null
+++ b/frontend/src/controllers/front_dashboard.js
@@ -0,0 +1,694 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers')
+    .controller('IndexDashboardController', IndexDashboardController);
+
+IndexDashboardController.$inject = ['$scope', '$location','$cookies', '$interval', 'stateHolder', 'userSelfPropertyResource', 'applicationsPropertyResource', 'AeConfig', 'AeUser'];
+
+function IndexDashboardController($scope, $location, $cookies, $interval, stateHolder, userSelfPropertyResource, applicationsPropertyResource, AeConfig, AeUser) {
+    var vm = this;
+    stateHolder.section = 'dashboard';
+    vm.timeOptions = {};
+    var allowed = ['1h', '4h', '12h', '24h', '1w', '2w', '1M'];
+    _.each(allowed, function (key) {
+        if (allowed.indexOf(key) !== -1) {
+            vm.timeOptions[key] = AeConfig.timeOptions[key];
+        }
+    });
+    vm.urls = AeConfig.urls;
+    vm.applications = AeUser.applications_map;
+    vm.show_dashboard = false;
+    vm.resource = null;
+    vm.graphType = {selected: null};
+    vm.timeSpan = vm.timeOptions['1h'];
+    vm.trendingReports = [];
+    vm.exceptions = 0;
+    vm.satisfyingRequests = 0;
+    vm.toleratedRequests = 0;
+    vm.frustratingRequests = 0;
+    vm.uptimeStats = 0;
+    vm.apdexStats = [];
+    vm.seriesRequestsData = [];
+    vm.seriesMetricsData = [];
+    vm.seriesSlowData = [];
+    vm.slowCalls = [];
+    vm.slowURIS = [];
+
+    vm.reportChartConfig = {
+        data: {
+            json: [],
+            xFormat: '%Y-%m-%dT%H:%M:%S'
+        },
+        color: {
+            pattern: ['#6baed6', '#e6550d', '#74c476', '#fdd0a2', '#8c564b']
+        },
+        axis: {
+            x: {
+                type: 'timeseries',
+                tick: {
+                    culling: {
+                        max: 6 // the number of tick texts will be adjusted to less than this value
+                    },
+                    format: '%Y-%m-%d %H:%M'
+                }
+            },
+            y: {
+                tick: {
+                    count: 5,
+                    format: d3.format('.2s')
+                }
+            }
+        },
+        subchart: {
+            show: true,
+            size: {
+                height: 20
+            }
+        },
+        size: {
+            height: 250
+        },
+        zoom: {
+            rescale: true
+        },
+        grid: {
+            x: {
+                show: true
+            },
+            y: {
+                show: true
+            }
+        },
+        tooltip: {
+            format: {
+                title: function (d) {
+                    return '' + d;
+                },
+                value: function (v) {
+                    return v
+                }
+            }
+        }
+    };
+    vm.reportChartData = {};
+
+    vm.reportSlowChartConfig = {
+        data: {
+            json: [],
+            xFormat: '%Y-%m-%dT%H:%M:%S'
+        },
+        color: {
+            pattern: ['#6baed6', '#e6550d', '#74c476', '#fdd0a2', '#8c564b']
+        },
+        axis: {
+            x: {
+                type: 'timeseries',
+                tick: {
+                    culling: {
+                        max: 6 // the number of tick texts will be adjusted to less than this value
+                    },
+                    format: '%Y-%m-%d %H:%M'
+                }
+            },
+            y: {
+                tick: {
+                    count: 5,
+                    format: d3.format('.2s')
+                }
+            }
+        },
+        subchart: {
+            show: true,
+            size: {
+                height: 20
+            }
+        },
+        size: {
+            height: 250
+        },
+        zoom: {
+            rescale: true
+        },
+        grid: {
+            x: {
+                show: true
+            },
+            y: {
+                show: true
+            }
+        },
+        tooltip: {
+            format: {
+                title: function (d) {
+                    return '' + d;
+                },
+                value: function (v) {
+                    return v
+                }
+            }
+        }
+    };
+    vm.reportSlowChartData = {};
+
+    vm.metricsChartConfig = {
+        data: {
+            json: [],
+            xFormat: '%Y-%m-%dT%H:%M:%S',
+            keys: {
+                x: 'x',
+                value: ["main", "sql", "nosql", "tmpl", "remote", "custom"]
+            },
+            names: {
+                main: 'View/Application logic',
+                sql: 'Relational database queries',
+                nosql: 'NoSql datastore calls',
+                tmpl: 'Template rendering',
+                custom: 'Custom timed calls',
+                remote: 'Requests to remote resources'
+            },
+            type: 'area',
+            groups: [["main", "sql", "nosql", "remote", "custom", "tmpl"]],
+            order: null
+        },
+        color: {
+            pattern: ['#6baed6', '#c7e9c0', '#fd8d3c', '#d6616b', '#ffcc00', '#c6dbef']
+        },
+        axis: {
+            x: {
+                type: 'timeseries',
+                tick: {
+                    culling: {
+                        max: 6 // the number of tick texts will be adjusted to less than this value
+                    },
+                    format: '%Y-%m-%d %H:%M'
+                }
+            },
+            y: {
+                tick: {
+                    count: 5,
+                    format: d3.format('.2f')
+                }
+            }
+        },
+        point: {
+            show: false
+        },
+        subchart: {
+            show: true,
+            size: {
+                height: 20
+            }
+        },
+        size: {
+            height: 350
+        },
+        zoom: {
+            rescale: true
+        },
+        grid: {
+            x: {
+                show: true
+            },
+            y: {
+                show: true
+            }
+        },
+        tooltip: {
+            format: {
+                title: function (d) {
+                    return '' + d;
+                },
+                value: function (v) {
+                    return v
+                }
+            }
+        }
+    };
+    vm.metricsChartData = {};
+
+    vm.responseChartConfig = {
+        data: {
+            json: [],
+            xFormat: '%Y-%m-%dT%H:%M:%S'
+        },
+        color: {
+            pattern: ['#d6616b', '#6baed6', '#fd8d3c']
+        },
+        axis: {
+            x: {
+                type: 'timeseries',
+                tick: {
+                    culling: {
+                        max: 6 // the number of tick texts will be adjusted to less than this value
+                    },
+                    format: '%Y-%m-%d %H:%M'
+                }
+            },
+            y: {
+                tick: {
+                    count: 5,
+                    format: d3.format('.2f')
+                }
+            }
+        },
+        point: {
+            show: false
+        },
+        subchart: {
+            show: true,
+            size: {
+                height: 20
+            }
+        },
+        size: {
+            height: 350
+        },
+        zoom: {
+            rescale: true
+        },
+        grid: {
+            x: {
+                show: true
+            },
+            y: {
+                show: true
+            }
+        },
+        tooltip: {
+            format: {
+                title: function (d) {
+                    return '' + d;
+                },
+                value: function (v) {
+                    return v
+                }
+            }
+        }
+    };
+    vm.responseChartData = {};
+
+    vm.requestsChartConfig = {
+        data: {
+            json: [],
+            xFormat: '%Y-%m-%dT%H:%M:%S'
+        },
+        color: {
+            pattern: ['#d6616b', '#6baed6', '#fd8d3c']
+        },
+        axis: {
+            x: {
+                type: 'timeseries',
+                tick: {
+                    culling: {
+                        max: 6 // the number of tick texts will be adjusted to less than this value
+                    },
+                    format: '%Y-%m-%d %H:%M'
+                }
+            },
+            y: {
+                tick: {
+                    count: 5,
+                    format: d3.format('.2f')
+                }
+            }
+        },
+        point: {
+            show: false
+        },
+        subchart: {
+            show: true,
+            size: {
+                height: 20
+            }
+        },
+        size: {
+            height: 350
+        },
+        zoom: {
+            rescale: true
+        },
+        grid: {
+            x: {
+                show: true
+            },
+            y: {
+                show: true
+            }
+        },
+        tooltip: {
+            format: {
+                title: function (d) {
+                    return '' + d;
+                },
+                value: function (v) {
+                    return v
+                }
+            }
+        }
+    };
+    vm.requestsChartData = {};
+
+    vm.loading = {
+        'apdex': true,
+        'reports': true,
+        'graphs': true,
+        'slowCalls': true,
+        'slowURIS': true,
+        'requestsBreakdown': true,
+        'series': true
+    };
+    vm.stream = {paused: false, filtered: false, messages: [], reports: []};
+    vm.websocket = null;
+    userSelfPropertyResource.get({key: 'websocket'}, function (data) {
+        console.log(data);
+        console.log(AeConfig.ws_url + '/ws?conn_id=' + data.conn_id);
+        vm.websocket = new ReconnectingWebSocket(AeConfig.ws_url + '/ws?conn_id=' + data.conn_id);
+        vm.websocket.onopen = function (event) {
+            console.log('open');
+        };
+        vm.websocket.onmessage = function (event) {
+            var data = JSON.parse(event.data);
+            console.log('message');
+            $scope.$apply(function (scope) {
+                _.each(data, function (message) {
+                    if (message.type === 'message'){
+                        ws_report = message.message.report;
+                        if (ws_report.http_status != 500) {
+                            return
+                        }
+                        if (vm.stream.paused == true) {
+                            return
+                        }
+                        if (vm.stream.filtered && ws_report.resource_id != vm.resource) {
+                            return
+                        }
+                        var should_insert = true;
+                        _.each(vm.stream.reports, function (report) {
+                            if (report.report_id == ws_report.report_id) {
+                                report.occurences = ws_report.occurences;
+                                should_insert = false;
+                            }
+                        });
+                        if (should_insert) {
+                            if (vm.stream.reports.length > 7) {
+                                vm.stream.reports.pop();
+                            }
+                            vm.stream.reports.unshift(ws_report);
+                        }
+                    }
+                });
+            });
+        };
+        vm.websocket.onclose = function (event) {
+            console.log('closed');
+        };
+
+        vm.websocket.onerror = function (event) {
+            console.log('error');
+        };
+
+    });
+
+    vm.determineStartState = function () {
+        if (AeUser.applications.length) {
+            vm.resource = Number($location.search().resource);
+
+            if (!vm.resource){
+                var cookieResource = $cookies.getObject('resource');
+                console.log('cookieResource', cookieResource);
+
+                if (cookieResource) {
+                    vm.resource = cookieResource;
+                }
+                else{
+                    vm.resource = AeUser.applications[0].resource_id;
+                }
+            }
+        }
+
+        var timespan = $location.search().timespan;
+
+        if(_.has(vm.timeOptions, timespan)){
+            vm.timeSpan = vm.timeOptions[timespan];
+        }
+        else{
+            vm.timeSpan = vm.timeOptions['1h'];
+        }
+
+        var graphType = $location.search().graphtype;
+        if(!graphType){
+            vm.graphType = {selected: 'metrics_graphs'};
+        }
+        else{
+            vm.graphType = {selected: graphType};
+        }
+        vm.updateSearchParams();
+    };
+
+    vm.updateSearchParams = function () {
+        $location.search('resource', vm.resource);
+        $location.search('timespan', vm.timeSpan.key);
+        $location.search('graphtype', vm.graphType.selected);
+        stateHolder.resource = vm.resource;
+        if (vm.resource){
+            $cookies.putObject('resource', vm.resource,
+                {expires:new Date(3000, 1, 1)});
+        }
+    };
+
+    vm.refreshData = function () {
+        vm.fetchApdexStats();
+        vm.fetchTrendingReports();
+        vm.fetchMetrics();
+        vm.fetchRequestsBreakdown();
+        vm.fetchSlowCalls();
+    }
+
+    vm.changedTimeSpan = function(){
+        vm.startDateFilter = timeSpanToStartDate(vm.timeSpan.key);
+        vm.refreshData();
+    }
+
+    var intervalId = $interval(function () {
+        if (_.contains(['30m', "1h"], vm.timeSpan.key)) {
+            vm.refreshData();
+        }
+    }, 60000);
+
+    $scope.$on('$destroy',function(){
+        $interval.cancel(intervalId);
+        vm.websocket.close();
+    });
+
+
+    vm.fetchApdexStats = function () {
+        vm.loading.apdex = true;
+        vm.apdexStats = applicationsPropertyResource.query({
+                'key': 'apdex_stats',
+                'resourceId': vm.resource,
+                "start_date": timeSpanToStartDate(vm.timeSpan.key)
+            },
+            function (data) {
+                vm.loading.apdex = false;
+
+                vm.exceptions = _.reduce(data, function (memo, row) {
+                    return memo + row.errors;
+                }, 0);
+                vm.satisfyingRequests = _.reduce(data, function (memo, row) {
+                    return memo + row.satisfying_requests;
+                }, 0);
+                vm.toleratedRequests = _.reduce(data, function (memo, row) {
+                    return memo + row.tolerated_requests;
+                }, 0);
+                vm.frustratingRequests = _.reduce(data, function (memo, row) {
+                    return memo + row.frustrating_requests;
+                }, 0);
+                if (data.length) {
+                    vm.uptimeStats = data[0].uptime;
+                }
+
+            },
+            function () {
+                vm.loading.apdex = false;
+            }
+        );
+    }
+
+    vm.fetchMetrics = function () {
+        vm.loading.series = true;
+        applicationsPropertyResource.query({
+            'resourceId': vm.resource,
+            'key': vm.graphType.selected,
+            "start_date": timeSpanToStartDate(vm.timeSpan.key)
+        }, function (data) {
+            if (vm.graphType.selected == 'metrics_graphs') {
+                vm.metricsChartData = {
+                    json: data,
+                    xFormat: '%Y-%m-%dT%H:%M:%S',
+                    keys: {
+                        x: 'x',
+                        value: ["main", "sql", "nosql", "tmpl", "remote", "custom"]
+                    },
+                    names: {
+                        main: 'View/Application logic',
+                        sql: 'Relational database queries',
+                        nosql: 'NoSql datastore calls',
+                        tmpl: 'Template rendering',
+                        custom: 'Custom timed calls',
+                        remote: 'Requests to remote resources'
+                    },
+                    type: 'area',
+                    groups: [["main", "sql", "nosql", "remote", "custom", "tmpl"]],
+                    order: null
+                };
+            }
+            else if (vm.graphType.selected == 'report_graphs') {
+                vm.reportChartData = {
+                    json: data,
+                    xFormat: '%Y-%m-%dT%H:%M:%S',
+                    keys: {
+                        x: 'x',
+                        value: ["not_found", "report"]
+                    },
+                    names: {
+                        report: 'Errors',
+                        not_found: '404\'s requests'
+                    },
+                    type: 'bar'
+                };
+            }
+            else if (vm.graphType.selected == 'slow_report_graphs') {
+                vm.reportSlowChartData = {
+                    json: data,
+                    xFormat: '%Y-%m-%dT%H:%M:%S',
+                    keys: {
+                        x: 'x',
+                        value: ["slow_report"]
+                    },
+                    names: {
+                        slow_report: 'Slow reports'
+                    },
+                    type: 'bar'
+                };
+            }
+            else if (vm.graphType.selected == 'response_graphs') {
+                vm.responseChartData = {
+                    json: data,
+                    xFormat: '%Y-%m-%dT%H:%M:%S',
+                    keys: {
+                        x: 'x',
+                        value: ["today", "days_ago_2", "days_ago_7"]
+                    },
+                    names: {
+                        today: 'Today',
+                        "days_ago_2": '2 days ago',
+                        "days_ago_7": '7 days ago'
+                    }
+                };
+            }
+            else if (vm.graphType.selected == 'requests_graphs') {
+                vm.requestsChartData = {
+                    json: data,
+                    xFormat: '%Y-%m-%dT%H:%M:%S',
+                    keys: {
+                        x: 'x',
+                        value: ["requests"]
+                    },
+                    names: {
+                        requests: 'Requests/s'
+                    }
+                };
+            }
+            vm.loading.series = false;
+        }, function(){
+            vm.loading.series = false;
+        });
+    }
+
+    vm.fetchSlowCalls = function () {
+        vm.loading.slowCalls = true;
+        applicationsPropertyResource.query({
+            'resourceId': vm.resource,
+            "start_date": timeSpanToStartDate(vm.timeSpan.key),
+            'key': 'slow_calls'
+        }, function (data) {
+            vm.slowCalls = data;
+            vm.loading.slowCalls = false;
+        }, function () {
+            vm.loading.slowCalls = false;
+        });
+    }
+
+    vm.fetchRequestsBreakdown = function () {
+        vm.loading.requestsBreakdown = true;
+        applicationsPropertyResource.query({
+            'resourceId': vm.resource,
+            "start_date": timeSpanToStartDate(vm.timeSpan.key),
+            'key': 'requests_breakdown'
+        }, function (data) {
+            vm.requestsBreakdown = data;
+            vm.loading.requestsBreakdown = false;
+        }, function () {
+            vm.loading.requestsBreakdown = false;
+        });
+    }
+
+    vm.fetchTrendingReports = function () {
+
+        if (vm.graphType.selected == 'slow_report_graphs') {
+            var report_type = 'slow';
+        }
+        else {
+            var report_type = 'error';
+        }
+
+        vm.loading.reports = true;
+        vm.trendingReports = applicationsPropertyResource.query({
+                'key': 'trending_reports',
+                'resourceId': vm.resource,
+                "start_date": timeSpanToStartDate(vm.timeSpan.key),
+                "report_type": report_type
+            },
+            function () {
+                vm.loading.reports = false;
+            },
+            function () {
+                vm.loading.reports = false;
+            }
+        )
+        ;
+    }
+
+    if (AeUser.applications.length){
+        vm.show_dashboard = true;
+        vm.determineStartState();
+        vm.refreshData();
+    }
+
+    $scope.$on('$locationChangeSuccess', function () {
+        console.log('$locationChangeSuccess IndexDashboardController');
+        if (vm.loading.series === false) {
+            vm.determineStartState();
+            vm.refreshData();
+        }
+    });
+
+
+}
diff --git a/frontend/src/controllers/header.js b/frontend/src/controllers/header.js
new file mode 100644
index 0000000..2b7c2a8
--- /dev/null
+++ b/frontend/src/controllers/header.js
@@ -0,0 +1,56 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers')
+    .controller('HeaderCtrl', HeaderCtrl);
+
+HeaderCtrl.$inject = ['$state', 'stateHolder', 'AeUser'];
+
+function HeaderCtrl($state, stateHolder, AeUser) {
+    var vm = this;
+    vm.stateHolder = stateHolder;
+    vm.assignedReports = AeUser.assigned_reports;
+    vm.latestEvents = AeUser.latest_events;
+    vm.activeEvents = 0;
+    _.each(vm.latestEvents, function (event) {
+        if (event.status === 1 && event.end_date === null) {
+            vm.activeEvents += 1;
+        }
+    });
+
+    vm.clickedEvent = function(event){
+        console.log(event);
+        // (from Event model)
+        // exception reports
+        if (_.contains([1,2], event.event_type)){
+            $state.go('report.list', {resource:event.resource_id, start_date:event.start_date});
+        }
+        // slowness reports
+        else if (_.contains([3,4], event.event_type)){
+            $state.go('report.list_slow', {resource:event.resource_id, start_date:event.start_date});
+        }
+        // uptime reports
+        else if (_.contains([7,8], event.event_type)){
+            $state.go('uptime', {resource:event.resource_id, start_date:event.start_date});
+        }
+        else{
+            console.log('other');
+        }
+    }
+}
diff --git a/frontend/src/controllers/index.js b/frontend/src/controllers/index.js
new file mode 100644
index 0000000..4e2afcb
--- /dev/null
+++ b/frontend/src/controllers/index.js
@@ -0,0 +1,27 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers').controller('IndexCtrl', IndexCtrl);
+
+IndexCtrl.$inject = [IndexCtrl];
+
+function IndexCtrl() {
+    var vm = this;
+    vm.selected_section = 'errors';
+}
diff --git a/frontend/src/controllers/index_test.js b/frontend/src/controllers/index_test.js
new file mode 100644
index 0000000..676ca7c
--- /dev/null
+++ b/frontend/src/controllers/index_test.js
@@ -0,0 +1,35 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+//describe('user login form', function() {
+//
+//    beforeEach(module('appenlight'));
+//
+//    // critical
+//    it('ensure invalid email addresses are caught', function() {
+//        browser().navigateTo('/');
+//    });
+//    it('ensure valid email addresses pass validation', function() {});
+//    it('ensure submitting form changes path', function() { });
+//
+//    // nice-to-haves
+//    it('ensure client-side helper shown for empty fields', function() { });
+//    it('ensure hitting enter on password field submits form', function() { });
+//
+//});
diff --git a/frontend/src/controllers/integrations/bitbucket.js b/frontend/src/controllers/integrations/bitbucket.js
new file mode 100644
index 0000000..75d2c0b
--- /dev/null
+++ b/frontend/src/controllers/integrations/bitbucket.js
@@ -0,0 +1,93 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers')
+    .controller('BitbucketIntegrationCtrl', BitbucketIntegrationCtrl)
+
+BitbucketIntegrationCtrl.$inject = ['$uibModalInstance', '$state', 'report', 'integrationName', 'integrationResource'];
+
+function BitbucketIntegrationCtrl($uibModalInstance, $state, report, integrationName, integrationResource) {
+    var vm = this;
+    vm.loading = true;
+    vm.assignees = [];
+    vm.report = report;
+    vm.integrationName = integrationName;
+    vm.statuses = [];
+    vm.priorities = [];
+    vm.error_messages = [];
+    vm.form = {
+        content: '\n' +
+        'Issue created for report: ' +
+        $state.href('report.view_detail', {groupId:report.group_id, reportId:report.id}, {absolute:true})
+    };
+
+    vm.fetchInfo = function () {
+        integrationResource.get({
+                resourceId: vm.report.resource_id,
+                action: 'info',
+                integration: vm.integrationName
+            }, null,
+            function (data) {
+                vm.loading = false;
+                if (data.error_messages) {
+                    vm.error_messages = data.error_messages;
+                }
+                vm.assignees = data.assignees;
+                vm.priorities = data.priorities;
+                vm.form.responsible = vm.assignees[0];
+                vm.form.priority = vm.priorities[0];
+            }, function (error_data) {
+                if (error_data.data.error_messages) {
+                    vm.error_messages = error_data.data.error_messages;
+                }
+                else {
+                    vm.error_messages = ['There was a problem processing your request'];
+                }
+            });
+    };
+    vm.ok = function () {
+        vm.loading = true;
+        vm.form.group_id = vm.report.group_id;
+        integrationResource.save({
+                resourceId: vm.report.resource_id,
+                action: 'create-issue',
+                integration: vm.integrationName
+            }, vm.form,
+            function (data) {
+                vm.loading = false;
+                if (data.error_messages) {
+                    vm.error_messages = data.error_messages;
+                }
+                if (data !== false) {
+                    $uibModalInstance.dismiss('success');
+                }
+            }, function (error_data) {
+                if (error_data.data.error_messages) {
+                    vm.error_messages = error_data.data.error_messages;
+                }
+                else {
+                    vm.error_messages = ['There was a problem processing your request'];
+                }
+            });
+    };
+    vm.cancel = function () {
+        $uibModalInstance.dismiss('cancel');
+    };
+    vm.fetchInfo();
+}
diff --git a/frontend/src/controllers/integrations/github.js b/frontend/src/controllers/integrations/github.js
new file mode 100644
index 0000000..bcd85f7
--- /dev/null
+++ b/frontend/src/controllers/integrations/github.js
@@ -0,0 +1,95 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers')
+    .controller('GithubIntegrationCtrl', GithubIntegrationCtrl);
+
+GithubIntegrationCtrl.$inject = ['$uibModalInstance', '$state', 'report', 'integrationName', 'integrationResource'];
+
+function GithubIntegrationCtrl($uibModalInstance, $state, report, integrationName, integrationResource) {
+    var vm = this;
+    vm.loading = true;
+    vm.assignees = [];
+    vm.report = report;
+    vm.integrationName = integrationName;
+    vm.statuses = [];
+    vm.assignees = [];
+    vm.error_messages = [];
+    vm.form = {
+        content: '\n' +
+        'Issue created for report: ' +
+        $state.href('report.view_detail', {groupId:report.group_id, reportId:report.id}, {absolute:true})
+    };
+
+    vm.fetchInfo = function () {
+        integrationResource.get({
+                resourceId: vm.report.resource_id,
+                action: 'info',
+                integration: vm.integrationName
+            }, null,
+            function (data) {
+                vm.loading = false;
+                if (data.error_messages) {
+                    vm.error_messages = data.error_messages;
+                }
+                else {
+                    vm.assignees = data.assignees;
+                    vm.statuses = data.statuses;
+                    vm.form.responsible = vm.assignees[0];
+                    vm.form.status = vm.statuses[0];
+                }
+            }, function (error_data) {
+                if (error_data.data.error_messages) {
+                    vm.error_messages = error_data.data.error_messages;
+                }
+                else {
+                    vm.error_messages = ['There was a problem processing your request'];
+                }
+            });
+    };
+    vm.ok = function () {
+        vm.loading = true;
+        vm.form.group_id = vm.report.group_id;
+        integrationResource.save({
+                resourceId: vm.report.resource_id,
+                action: 'create-issue',
+                integration: vm.integrationName
+            }, vm.form,
+            function (data) {
+                vm.loading = false;
+                if (data.error_messages) {
+                    vm.error_messages = data.error_messages;
+                }
+                else {
+                    $uibModalInstance.dismiss('success');
+                }
+            }, function (error_data) {
+                if (error_data.data.error_messages) {
+                    vm.error_messages = error_data.data.error_messages;
+                }
+                else {
+                    vm.error_messages = ['There was a problem processing your request'];
+                }
+            });
+    };
+    vm.cancel = function () {
+        $uibModalInstance.dismiss('cancel');
+    };
+    vm.fetchInfo();
+}
diff --git a/frontend/src/controllers/integrations/jira.js b/frontend/src/controllers/integrations/jira.js
new file mode 100644
index 0000000..7c66066
--- /dev/null
+++ b/frontend/src/controllers/integrations/jira.js
@@ -0,0 +1,94 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers')
+    .controller('JiraIntegrationCtrl', JiraIntegrationCtrl)
+
+JiraIntegrationCtrl.$inject = ['$uibModalInstance', '$state', 'report', 'integrationName', 'integrationResource'];
+
+function JiraIntegrationCtrl($uibModalInstance, $state, report, integrationName, integrationResource) {
+    var vm = this;
+    vm.loading = true;
+    vm.assignees = [];
+    vm.report = report;
+    vm.integrationName = integrationName;
+    vm.statuses = [];
+    vm.priorities = [];
+    vm.error_messages = [];
+    vm.form = {
+        content: '\n' +
+        'Issue created for report: ' +
+        $state.href('report.view_detail', {groupId:report.group_id, reportId:report.id}, {absolute:true})
+    };
+
+    vm.fetchInfo = function () {
+        integrationResource.get({
+                resourceId: vm.report.resource_id,
+                action: 'info',
+                integration: vm.integrationName
+            }, null,
+            function (data) {
+                vm.loading = false;
+                if (data.error_messages) {
+                    vm.error_messages = data.error_messages;
+                }
+                vm.assignees = data.assignees;
+                vm.priorities = data.priorities;
+                vm.form.responsible = vm.assignees[0];
+                vm.form.priority = vm.priorities[0];
+            }, function (error_data) {
+                console.log('ERROR');
+                if (error_data.data.error_messages) {
+                    vm.error_messages = error_data.data.error_messages;
+                }
+                else {
+                    vm.error_messages = ['There was a problem processing your request'];
+                }
+            });
+    };
+    vm.ok = function () {
+        vm.loading = true;
+        vm.form.group_id = vm.report.group_id;
+        integrationResource.save({
+                resourceId: vm.report.resource_id,
+                action: 'create-issue',
+                integration: vm.integrationName
+            }, vm.form,
+            function (data) {
+                vm.loading = false;
+                if (data.error_messages) {
+                    vm.error_messages = data.error_messages;
+                }
+                if (data !== false) {
+                    $uibModalInstance.dismiss('success');
+                }
+            }, function (error_data) {
+                if (error_data.data.error_messages) {
+                    vm.error_messages = error_data.data.error_messages;
+                }
+                else {
+                    vm.error_messages = ['There was a problem processing your request'];
+                }
+            });
+    };
+    vm.cancel = function () {
+        $uibModalInstance.dismiss('cancel');
+    };
+    vm.fetchInfo();
+}
diff --git a/frontend/src/controllers/logs.js b/frontend/src/controllers/logs.js
new file mode 100644
index 0000000..3908553
--- /dev/null
+++ b/frontend/src/controllers/logs.js
@@ -0,0 +1,299 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers').controller('LogsController', LogsController);
+
+LogsController.$inject = ['$scope', '$location', 'stateHolder', 'typeAheadTagHelper', 'logsNoIdResource', 'sectionViewResource', 'AeUser'];
+
+function LogsController($scope, $location, stateHolder, typeAheadTagHelper, logsNoIdResource, sectionViewResource, AeUser) {
+    var vm = this;
+    vm.logEventsChartConfig = {
+        data: {
+            json: [],
+            xFormat: '%Y-%m-%dT%H:%M:%S'
+        },
+        color: {
+            pattern: ['#6baed6', '#e6550d', '#74c476', '#fdd0a2', '#8c564b']
+        },
+        axis: {
+            x: {
+                type: 'timeseries',
+                tick: {
+                    format: '%Y-%m-%d'
+                }
+            },
+            y: {
+                tick: {
+                    count: 5,
+                    format: d3.format('.2s')
+                }
+            }
+        },
+        subchart: {
+            show: true,
+            size: {
+                height: 20
+            }
+        },
+        size: {
+            height: 250
+        },
+        zoom: {
+            rescale: true
+        },
+        grid: {
+            x: {
+                show: true
+            },
+            y: {
+                show: true
+            }
+        },
+        tooltip: {
+            format: {
+                title: function (d) {
+                    return '' + d;
+                },
+                value: function (v) {
+                    return v
+                }
+            }
+        }
+    };
+    vm.logEventsChartData = {};
+    stateHolder.section = 'logs';
+    vm.today = function () {
+        vm.pickerDate = new Date();
+    };
+    vm.today();
+
+    vm.applications = AeUser.applications_map;
+    vm.logsPage = [];
+    vm.itemCount = 0;
+    vm.itemsPerPage = 250;
+    vm.searchParams = parseSearchToTags($location.search());
+    vm.$location = $location;
+    vm.isLoading = {
+        logs: true,
+        series: true
+    };
+    vm.filterTypeAheadOptions = [
+        {
+            type: 'message',
+            text: 'message:',
+            'description': 'Full-text search in your logs',
+            tag: 'Message',
+            example: 'message:text-im-looking-for'
+        },
+        {
+            type: 'namespace',
+            text: 'namespace:',
+            'description': 'Query logs from specific namespace',
+            tag: 'Namespace',
+            example: "namespace:module.foo"
+        },
+        {
+            type: 'resource',
+            text: 'resource:',
+            'description': 'Restrict resultset to application',
+            tag: 'Application',
+            example: "resource:ID"
+        },
+        {
+            type: 'request_id',
+            text: 'request_id:',
+            'description': 'Show logs with specific request id',
+            example: "request_id:883143dc572e4c38aceae92af0ea5ae0",
+            tag: 'Request ID'
+        },
+        {
+            type: 'level',
+            text: 'level:',
+            'description': 'Show entries with specific log level',
+            example: 'level:warning',
+            tag: 'Level'
+        },
+        {
+            type: 'server_name',
+            text: 'server_name:',
+            'description': 'Show entries tagged with this key/value pair',
+            example: 'server_name:hostname',
+            tag: 'Tag'
+        },
+        {
+            type: 'start_date',
+            text: 'start_date:',
+            'description': 'Show results newer than this date (press TAB for dropdown)',
+            example: 'start_date:2014-08-15T13:00',
+            tag: 'Start Date'
+        },
+        {
+            type: 'end_date',
+            text: 'end_date:',
+            'description': 'Show results older than this date (press TAB for dropdown)',
+            example: 'start_date:2014-08-15T23:59',
+            tag: 'End Date'
+        },
+        {type: 'level', value: 'debug', text: 'level:debug'},
+        {type: 'level', value: 'info', text: 'level:info'},
+        {type: 'level', value: 'warning', text: 'level:warning'},
+        {type: 'level', value: 'critical', text: 'level:critical'}
+    ];
+    vm.filterTypeAhead = null;
+    vm.showDatePicker = false;
+    vm.manualOpen = false;
+    vm.aheadFilter = typeAheadTagHelper.aheadFilter;
+    vm.removeSearchTag = function (tag) {
+        $location.search(tag.type, null);
+    };
+    vm.addSearchTag = function (tag) {
+        $location.search(tag.type, tag.value);
+    };
+
+    vm.paginationChange = function(){
+        $location.search('page', vm.searchParams.page);
+    };
+
+
+    _.each(vm.applications, function (item) {
+        vm.filterTypeAheadOptions.push({
+            type: 'resource',
+            text: 'resource:' + item.resource_id + ':' + item.resource_name,
+            example: 'resource:' + item.resource_id,
+            'tag': item.resource_name,
+            'description': 'Restrict resultset to this application'
+        });
+    });
+
+    vm.typeAheadTag = function (event) {
+        var text = vm.filterTypeAhead;
+        if (_.isObject(vm.filterTypeAhead)) {
+            text = vm.filterTypeAhead.text;
+        }
+        ;
+        if (!vm.filterTypeAhead) {
+            return
+        }
+        var parsed = text.split(':');
+        var tag = {'type': null, 'value': null};
+        // app tags have : twice
+        if (parsed.length > 2 && parsed[0] == 'resource') {
+            tag.type = 'resource';
+            tag.value = parsed[1];
+        }
+        // normal tag:value
+        else if (parsed.length > 1) {
+            tag.type = parsed[0];
+            tag.value = parsed.slice(1).join(':');
+        }
+        else {
+            tag.type = 'message';
+            tag.value = parsed.join(':');
+        }
+
+        // set datepicker hour based on type of field
+        if ('start_date:' == text) {
+            vm.showDatePicker = true;
+            vm.filterTypeAhead = 'start_date:' + moment(vm.pickerDate).utc().format();
+        }
+        else if ('end_date:' == text) {
+            vm.showDatePicker = true;
+            vm.filterTypeAhead = 'end_date:' + moment(vm.pickerDate).utc().hour(23).minute(59).format();
+        }
+
+        if (event.keyCode != 13 || !tag.type || !tag.value) {
+            return
+        }
+        vm.showDatePicker = false;
+        // aka we selected one of main options
+        $location.search(tag.type, tag.value);
+
+        // clear typeahead
+        vm.filterTypeAhead = undefined;
+    };
+
+
+    vm.pickerDateChanged = function(){
+        if (vm.filterTypeAhead.indexOf('start_date:') == '0') {
+            vm.filterTypeAhead = 'start_date:' + moment(vm.pickerDate).utc().format();
+        }
+        else if (vm.filterTypeAhead.indexOf('end_date:') == '0') {
+            vm.filterTypeAhead = 'end_date:' + moment(vm.pickerDate).utc().hour(23).minute(59).format();
+        }
+        vm.showDatePicker = false;
+    };
+
+    vm.fetchLogs = function (searchParams) {
+        vm.isLoading.logs = true;
+        logsNoIdResource.query(searchParams, function (data, getResponseHeaders) {
+            vm.isLoading.logs = false;
+            var headers = getResponseHeaders();
+            vm.logsPage = data;
+            vm.itemCount = headers['x-total-count'];
+            vm.itemsPerPage = headers['x-items-per-page'];
+        }, function () {
+            vm.isLoading.logs = false;
+        });
+    };
+
+    vm.fetchSeriesData = function (searchParams) {
+        searchParams['section'] = 'logs_section';
+        searchParams['view'] = 'fetch_series';
+        vm.isLoading.series = true;
+        sectionViewResource.query(searchParams, function (data) {
+            console.log('show node here');
+            vm.logEventsChartData = {
+                json: data,
+                xFormat: '%Y-%m-%dT%H:%M:%S',
+                keys: {
+                    x: 'x',
+                    value: ["logs"]
+                },
+                names: {
+                    logs: 'Log events'
+                },
+                type: 'bar'
+            };
+            vm.isLoading.series = false;
+        }, function () {
+            vm.isLoading.series = false;
+        });
+    };
+
+    vm.filterId = function (log) {
+        $location.search('request_id', log.request_id);
+    };
+
+    var params = parseTagsToSearch(vm.searchParams);
+    vm.fetchLogs(params);
+    vm.fetchSeriesData(params);
+
+    $scope.$on('$locationChangeSuccess', function () {
+        console.log('$locationChangeSuccess LogsController');
+        vm.searchParams = parseSearchToTags($location.search());
+        var params = parseTagsToSearch(vm.searchParams);
+        console.log($location.path());
+        if (vm.isLoading.logs === false) {
+            console.log(params);
+            vm.fetchLogs(params);
+            vm.fetchSeriesData(params);
+        }
+    });
+
+}
diff --git a/frontend/src/controllers/overview.js b/frontend/src/controllers/overview.js
new file mode 100644
index 0000000..c828921
--- /dev/null
+++ b/frontend/src/controllers/overview.js
@@ -0,0 +1,27 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers')
+    .controller('OverviewCtrl', OverviewCtrl);
+
+OverviewCtrl.$inject = [];
+
+function OverviewCtrl() {
+
+}
diff --git a/frontend/src/controllers/register.js b/frontend/src/controllers/register.js
new file mode 100644
index 0000000..ddbffe8
--- /dev/null
+++ b/frontend/src/controllers/register.js
@@ -0,0 +1,31 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers')
+    .controller('RegisterController', RegisterController);
+
+RegisterController.$inject = ['$scope', '$location'];
+
+function RegisterController() {
+    var vm = this;
+    vm.selected_form = 'sign-up';
+    if (window.location.search.indexOf('sign_in') != -1 || window.location.search.indexOf('came_from') != -1) {
+        vm.selected_form = 'sign-in';
+    }
+}
diff --git a/frontend/src/controllers/reports/assign_report.js b/frontend/src/controllers/reports/assign_report.js
new file mode 100644
index 0000000..c8e5fe1
--- /dev/null
+++ b/frontend/src/controllers/reports/assign_report.js
@@ -0,0 +1,85 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers').controller('AssignReportCtrl', AssignReportCtrl);
+AssignReportCtrl.$inject = ['$uibModalInstance', 'reportGroupPropertyResource', 'report'];
+
+function AssignReportCtrl($uibModalInstance, reportGroupPropertyResource, report) {
+    var vm = this;
+    vm.loading = true;
+    vm.assignedUsers = [];
+    vm.unAssignedUsers = [];
+    vm.report = report;
+    vm.fetchAssignments = function () {
+        reportGroupPropertyResource.get({
+                groupId: vm.report.group_id,
+                key: 'assigned_users'
+            }, null,
+            function (data) {
+                vm.assignedUsers = data.assigned;
+                vm.unAssignedUsers = data.unassigned;
+                vm.loading = false;
+            });
+    }
+
+    vm.reassignUser = function (user) {
+        var is_assigned = vm.assignedUsers.indexOf(user);
+        if (is_assigned != -1) {
+            vm.assignedUsers.splice(is_assigned, 1);
+            vm.unAssignedUsers.push(user);
+            return
+        }
+        var is_unassigned = vm.unAssignedUsers.indexOf(user);
+        if (is_unassigned != -1) {
+            vm.unAssignedUsers.splice(is_unassigned, 1);
+            vm.assignedUsers.push(user);
+            return
+        }
+    }
+    vm.updateAssignments = function () {
+        var post = {'unassigned': [], 'assigned': []};
+        _.each(vm.assignedUsers, function (u) {
+            post['assigned'].push(u.user_name)
+        });
+        _.each(vm.unAssignedUsers, function (u) {
+            post['unassigned'].push(u.user_name)
+        });
+        vm.loading = true;
+        reportGroupPropertyResource.update({
+                groupId: vm.report.group_id,
+                key: 'assigned_users'
+            }, post,
+            function (data) {
+                vm.loading = false;
+                $uibModalInstance.close(vm.report);
+            });
+    };
+
+
+    vm.ok = function () {
+        vm.updateAssignments();
+    };
+
+    vm.cancel = function () {
+        $uibModalInstance.dismiss('cancel');
+    };
+
+    vm.fetchAssignments();
+
+}
diff --git a/frontend/src/controllers/reports/list.js b/frontend/src/controllers/reports/list.js
new file mode 100644
index 0000000..72fb2a7
--- /dev/null
+++ b/frontend/src/controllers/reports/list.js
@@ -0,0 +1,320 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers')
+    .controller('ReportsListController', ReportsListController);
+
+ReportsListController.$inject = ['$scope', '$location', '$cookies',
+    'stateHolder', 'typeAheadTagHelper', 'reportsResource', 'AeUser'];
+
+function ReportsListController($scope, $location, $cookies, stateHolder,
+                               typeAheadTagHelper, reportsResource, AeUser) {
+    var vm = this;
+    vm.applications = AeUser.applications_map;
+    stateHolder.section = 'reports';
+    vm.today = function () {
+        vm.pickerDate = new Date();
+    };
+    vm.today();
+    vm.reportsPage = [];
+    vm.itemCount = 0;
+    vm.itemsPerPage = 250;
+    typeAheadTagHelper.tags = [];
+    vm.searchParams = {tags: [], page: 1, type: 'report'};
+    vm.searchParams = parseSearchToTags($location.search());
+    vm.is_loading = false;
+    vm.filterTypeAheadOptions = [
+        {
+            type: 'error',
+            text: 'error:',
+            'description': 'Full-text search in your reports',
+            example: 'error:text-im-looking-for',
+            tag: 'Error'
+        },
+        {
+            type: 'view_name',
+            text: 'view_name:',
+            'description': 'Query reports occured in specific views',
+            example: "view_name:module.foo",
+            tag: 'View Name'
+        },
+        {
+            type: 'resource',
+            text: 'resource:',
+            'description': 'Restrict resultset to application',
+            example: "resource:ID",
+            tag: 'Application'
+        },
+        {
+            type: 'priority',
+            text: 'priority:',
+            'description': 'Show reports with specific priority',
+            example: 'priority:8',
+            tag: 'Priority'
+        },
+        {
+            type: 'min_occurences',
+            text: 'min_occurences:',
+            'description': 'Show reports from groups with at least X occurences',
+            example: 'min_occurences:25',
+            tag: 'Occurences'
+        },
+        {
+            type: 'url_path',
+            text: 'url_path:',
+            'description': 'Show reports from specific URL paths',
+            example: 'url_path:/foo/bar/baz',
+            tag: 'Url Path'
+        },
+        {
+            type: 'url_domain',
+            text: 'url_domain:',
+            'description': 'Show reports from specific domain',
+            example: 'url_domain:domain.com',
+            tag: 'Domain'
+        },
+        {
+            type: 'report_status',
+            text: 'report_status:',
+            'description': 'Show reports from groups with specific status',
+            example: 'report_status:never_reviewed',
+            tag: 'Status'
+        },
+        {
+            type: 'request_id',
+            text: 'request_id:',
+            'description': 'Show reports with specific request id',
+            example: "request_id:883143dc572e4c38aceae92af0ea5ae0",
+            tag: 'Request ID'
+        },
+        {
+            type: 'server_name',
+            text: 'server_name:',
+            'description': 'Show reports tagged with this key/value pair',
+            example: 'server_name:hostname',
+            tag: 'Tag'
+        },
+        {
+            type: 'http_status',
+            text: 'http_status:',
+            'description': 'Show reports with specific HTTP status code',
+            example: "http_status:",
+            tag: 'HTTP Status'
+        },
+        {
+            type: 'http_status',
+            text: 'http_status:500',
+            'description': 'Show reports with specific HTTP status code',
+            example: "http_status:500",
+            tag: 'HTTP Status'
+        },
+        {
+            type: 'http_status',
+            text: 'http_status:404',
+            'description': 'Include 404 reports in your search',
+            example: "http_status:404",
+            tag: 'HTTP Status'
+        },
+        {
+            type: 'start_date',
+            text: 'start_date:',
+            'description': 'Show reports newer than this date (press TAB for dropdown)',
+            example: 'start_date:2014-08-15T13:00',
+            tag: 'Start Date'
+        },
+        {
+            type: 'end_date',
+            text: 'end_date:',
+            'description': 'Show reports older than this date (press TAB for dropdown)',
+            example: 'start_date:2014-08-15T23:59',
+            tag: 'End Date'
+        }
+    ];
+
+    vm.filterTypeAhead = undefined;
+    vm.showDatePicker = false;
+    vm.manualOpen = false;
+    vm.aheadFilter = typeAheadTagHelper.aheadFilter;
+    vm.removeSearchTag = function (tag) {
+        $location.search(tag.type, null);
+    };
+    vm.addSearchTag = function (tag) {
+        $location.search(tag.type, tag.value);
+    };
+    vm.notRelativeTime = false;
+    if ($cookies.notRelativeTime) {
+        vm.notRelativeTime = JSON.parse($cookies.notRelativeTime);
+    }
+
+    vm.changeRelativeTime = function () {
+        $cookies.notRelativeTime = JSON.stringify(vm.notRelativeTime);
+    }
+
+    _.each(_.range(1, 11), function (priority) {
+        vm.filterTypeAheadOptions.push({
+            type: 'priority',
+            text: 'priority:' + priority.toString(),
+            description: 'Show entries with specific priority',
+            example: 'priority:' + priority,
+            tag: 'Priority'
+        });
+    });
+    _.each(['never_reviewed', 'reviewed', 'fixed', 'public'], function (status) {
+        vm.filterTypeAheadOptions.push({
+            type: 'report_status',
+            text: 'report_status:' + status,
+            'description': 'Show only reports with this status',
+            example: 'report_status:' + status,
+            tag: 'Status ' + status.toUpperCase()
+        });
+    });
+    _.each(AeUser.applications, function (item) {
+        vm.filterTypeAheadOptions.push({
+            type: 'resource',
+            text: 'resource:' + item.resource_id + ':' + item.resource_name,
+            example: 'resource:' + item.resource_id,
+            'tag': item.resource_name,
+            'description': 'Restrict resultset to this application'
+        });
+    });
+
+    vm.paginationChange = function(){
+        $location.search('page', vm.searchParams.page);
+    };
+
+    vm.typeAheadTag = function (event) {
+        var text = vm.filterTypeAhead;
+        if (_.isObject(vm.filterTypeAhead)) {
+            text = vm.filterTypeAhead.text;
+        }
+        if (!vm.filterTypeAhead) {
+            return
+        }
+
+        var parsed = text.split(':');
+        var tag = {'type': null, 'value': null};
+        // app tags have : twice
+        if (parsed.length > 2 && parsed[0] == 'resource') {
+            tag.type = 'resource';
+            tag.value = parsed[1];
+        }
+        // normal tag:value
+        else if (parsed.length > 1) {
+            tag.type = parsed[0];
+            var tagValue = parsed.slice(1);
+            if (tagValue) {
+                tag.value = tagValue.join(':');
+            }
+        }
+        else {
+            tag.type = 'error';
+            tag.value = parsed.join(':');
+        }
+
+        // set datepicker hour based on type of field
+        if ('start_date:' == text) {
+            vm.showDatePicker = true;
+            vm.filterTypeAhead = 'start_date:' + moment(vm.pickerDate).utc().format();
+        }
+        else if ('end_date:' == text) {
+            vm.showDatePicker = true;
+            vm.filterTypeAhead = 'end_date:' + moment(vm.pickerDate).utc().hour(23).minute(59).format();
+        }
+
+        if (event.keyCode != 13 || !tag.type || !tag.value) {
+            return
+        }
+        vm.showDatePicker = false;
+        // aka we selected one of main options
+        $location.search(tag.type, tag.value);
+        // clear typeahead
+        vm.filterTypeAhead = undefined;
+    }
+
+    vm.pickerDateChanged = function(){
+        if (vm.filterTypeAhead.indexOf('start_date:') == '0') {
+            vm.filterTypeAhead = 'start_date:' + moment(vm.pickerDate).utc().format();
+        }
+        else if (vm.filterTypeAhead.indexOf('end_date:') == '0') {
+            vm.filterTypeAhead = 'end_date:' + moment(vm.pickerDate).utc().hour(23).minute(59).format();
+        }
+        vm.showDatePicker = false;
+    };
+
+    var reportPresentation = function (report) {
+        report.presentation = {};
+        if (report.group.public) {
+            report.presentation.className = 'public';
+            report.presentation.tooltip = 'Public';
+        }
+        else if (report.group.fixed) {
+            report.presentation.className = 'fixed';
+            report.presentation.tooltip = 'Fixed';
+        }
+        else if (report.group.read) {
+            report.presentation.className = 'reviewed';
+            report.presentation.tooltip = 'Reviewed';
+        }
+        else {
+            report.presentation.className = 'new';
+            report.presentation.tooltip = 'New';
+        }
+        return report;
+    };
+
+    vm.fetchReports = function (searchParams) {
+        vm.is_loading = true;
+        reportsResource.query(searchParams, function (data, getResponseHeaders) {
+            var headers = getResponseHeaders();
+            console.log(headers);
+            vm.is_loading = false;
+            vm.reportsPage = _.map(data, function (item) {
+                return reportPresentation(item);
+            });
+            vm.itemCount = headers['x-total-count'];
+            vm.itemsPerPage = headers['x-items-per-page'];
+        }, function () {
+            vm.is_loading = false;
+        });
+    };
+
+    vm.filterId = function (log) {
+        vm.searchParams.tags.push({
+            type: "request_id",
+            value: log.request_id
+        });
+    };
+    // initial load
+    var params = parseTagsToSearch(vm.searchParams);
+    vm.fetchReports(params);
+
+    $scope.$on('$locationChangeSuccess', function () {
+        console.log('$locationChangeSuccess ReportsListController ');
+        vm.searchParams = parseSearchToTags($location.search());
+        var params = parseTagsToSearch(vm.searchParams);
+        console.log($location.path());
+        if (vm.is_loading === false) {
+            console.log(params);
+            vm.fetchReports(params);
+        }
+
+    });
+
+
+}
diff --git a/frontend/src/controllers/reports/list_slow.js b/frontend/src/controllers/reports/list_slow.js
new file mode 100644
index 0000000..4a1b2c3
--- /dev/null
+++ b/frontend/src/controllers/reports/list_slow.js
@@ -0,0 +1,298 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+'use strict';
+
+/* Controllers */
+
+angular.module('appenlight.controllers')
+    .controller('ReportsListSlowController', ReportsListSlowController);
+
+ReportsListSlowController.$inject = ['$scope', '$location', '$cookies',
+    'stateHolder', 'typeAheadTagHelper', 'slowReportsResource', 'AeUser']
+
+function ReportsListSlowController($scope, $location, $cookies, stateHolder, typeAheadTagHelper, slowReportsResource, AeUser) {
+    var vm = this;
+    vm.applications = AeUser.applications_map;
+    stateHolder.section = 'slow_reports';
+    vm.today = function () {
+        vm.pickerDate = new Date();
+    };
+    vm.today();
+    vm.reportsPage = [];
+    vm.itemCount = 0;
+    vm.itemsPerPage = 250;
+    typeAheadTagHelper.tags = [];
+    vm.searchParams = {tags: [], page: 1, type: 'slow_report'};
+    vm.searchParams = parseSearchToTags($location.search());
+    vm.is_loading = false;
+    vm.filterTypeAheadOptions = [
+        {
+            type: 'view_name',
+            text: 'view_name:',
+            'description': 'Query reports occured in specific views',
+            tag: 'View Name',
+            example: "view_name:module.foo"
+        },
+        {
+            type: 'resource',
+            text: 'resource:',
+            'description': 'Restrict resultset to application',
+            tag: 'Application',
+            example: "resource:ID"
+        },
+        {
+            type: 'priority',
+            text: 'priority:',
+            'description': 'Show reports with specific priority',
+            example: 'priority:8',
+            tag: 'Priority'
+        },
+        {
+            type: 'min_occurences',
+            text: 'min_occurences:',
+            'description': 'Show reports from groups with at least X occurences',
+            example: 'min_occurences:25',
+            tag: 'Min. occurences'
+        },
+        {
+            type: 'min_duration',
+            text: 'min_duration:',
+            'description': 'Show reports from groups with average duration >= Xs',
+            example: 'min_duration:4.5',
+            tag: 'Min. duration'
+        },
+        {
+            type: 'url_path',
+            text: 'url_path:',
+            'description': 'Show reports from specific URL paths',
+            example: 'url_path:/foo/bar/baz',
+            tag: 'Url Path'
+        },
+        {
+            type: 'url_domain',
+            text: 'url_domain:',
+            'description': 'Show reports from specific domain',
+            example: 'url_domain:domain.com',
+            tag: 'Domain'
+        },
+        {
+            type: 'request_id',
+            text: 'request_id:',
+            'description': 'Show reports with specific request id',
+            example: "request_id:883143dc572e4c38aceae92af0ea5ae0",
+            tag: 'Request ID'
+        },
+        {
+            type: 'report_status',
+            text: 'report_status:',
+            'description': 'Show reports from groups with specific status',
+            example: 'report_status:never_reviewed',
+            tag: 'Status'
+        },
+        {
+            type: 'server_name',
+            text: 'server_name:',
+            'description': 'Show reports tagged with this key/value pair',
+            example: 'server_name:hostname',
+            tag: 'Tag'
+        },
+        {
+            type: 'start_date',
+            text: 'start_date:',
+            'description': 'Show reports newer than this date (press TAB for dropdown)',
+            example: 'start_date:2014-08-15T13:00',
+            tag: 'Start Date'
+        },
+        {
+            type: 'end_date',
+            text: 'end_date:',
+            'description': 'Show reports older than this date (press TAB for dropdown)',
+            example: 'start_date:2014-08-15T23:59',
+            tag: 'End Date'
+        }
+    ];
+
+    vm.filterTypeAhead = undefined;
+    vm.showDatePicker = false;
+    vm.aheadFilter = typeAheadTagHelper.aheadFilter;
+    vm.removeSearchTag = function (tag) {
+        $location.search(tag.type, null);
+    };
+    vm.addSearchTag = function (tag) {
+        $location.search(tag.type, tag.value);
+    };
+    vm.manualOpen = false;
+    vm.notRelativeTime = false;
+    if ($cookies.notRelativeTime) {
+        vm.notRelativeTime = JSON.parse($cookies.notRelativeTime);
+    }
+
+
+    vm.changeRelativeTime = function () {
+        $cookies.notRelativeTime = JSON.stringify(vm.notRelativeTime);
+    };
+
+    _.each(_.range(1, 11), function (priority) {
+        vm.filterTypeAheadOptions.push({
+            type: 'priority',
+            text: 'priority:' + priority.toString(),
+            description: 'Show entries with specific priority',
+            example: 'priority:' + priority,
+            tag: 'Priority'
+        });
+    });
+    _.each(['never_reviewed', 'reviewed', 'fixed', 'public'], function (status) {
+        vm.filterTypeAheadOptions.push({
+            type: 'report_status',
+            text: 'report_status:' + status,
+            'description': 'Show only reports with this status',
+            example: 'report_status:' + status,
+            tag: 'Status ' + status.toUpperCase()
+        });
+    });
+    _.each(AeUser.applications, function (item) {
+        vm.filterTypeAheadOptions.push({
+            type: 'resource',
+            text: 'resource:' + item.resource_id + ':' + item.resource_name,
+            example: 'resource:' + item.resource_id,
+            'tag': item.resource_name,
+            'description': 'Restrict resultset to this application'
+        });
+    });
+
+    vm.typeAheadTag = function (event) {
+        var text = vm.filterTypeAhead;
+        if (_.isObject(vm.filterTypeAhead)) {
+            text = vm.filterTypeAhead.text;
+        }
+        ;
+        if (!vm.filterTypeAhead) {
+            return
+        }
+        var parsed = text.split(':');
+        var tag = {'type': null, 'value': null};
+        // app tags have : twice
+        if (parsed.length > 2 && parsed[0] == 'resource') {
+            tag.type = 'resource';
+            tag.value = parsed[1];
+        }
+        // normal tag:value
+        else if (parsed.length > 1) {
+            tag.type = parsed[0];
+            var tagValue = parsed.slice(1);
+            if (tagValue) {
+                tag.value = tagValue.join(':');
+            }
+        }
+
+        // set datepicker hour based on type of field
+        if ('start_date:' == text) {
+            vm.showDatePicker = true;
+            vm.filterTypeAhead = 'start_date:' + moment(vm.pickerDate).utc().format();
+        }
+        else if ('end_date:' == text) {
+            vm.showDatePicker = true;
+            vm.filterTypeAhead = 'end_date:' + moment(vm.pickerDate).utc().hour(23).minute(59).format();
+        }
+
+        if (event.keyCode != 13 || !tag.type || !tag.value) {
+            return
+        }
+        vm.showDatePicker = false;
+        // aka we selected one of main options
+        $location.search(tag.type, tag.value);
+        // clear typeahead
+        vm.filterTypeAhead = undefined;
+    }
+
+    vm.paginationChange = function(){
+        $location.search('page', vm.searchParams.page);
+    }
+
+    vm.pickerDateChanged = function(){
+        if (vm.filterTypeAhead.indexOf('start_date:') == '0') {
+            vm.filterTypeAhead = 'start_date:' + moment(vm.pickerDate).utc().format();
+        }
+        else if (vm.filterTypeAhead.indexOf('end_date:') == '0') {
+            vm.filterTypeAhead = 'end_date:' + moment(vm.pickerDate).utc().hour(23).minute(59).format();
+        }
+        vm.showDatePicker = false;
+    }
+
+    var reportPresentation = function (report) {
+        report.presentation = {};
+        if (report.group.public) {
+            report.presentation.className = 'public';
+            report.presentation.tooltip = 'Public';
+        }
+        else if (report.group.fixed) {
+            report.presentation.className = 'fixed';
+            report.presentation.tooltip = 'Fixed';
+        }
+        else if (report.group.read) {
+            report.presentation.className = 'reviewed';
+            report.presentation.tooltip = 'Reviewed';
+        }
+        else {
+            report.presentation.className = 'new';
+            report.presentation.tooltip = 'New';
+        }
+        return report;
+    }
+
+    vm.fetchReports = function (searchParams) {
+        vm.is_loading = true;
+        slowReportsResource.query(searchParams, function (data, getResponseHeaders) {
+            var headers = getResponseHeaders();
+            console.log(headers);
+            vm.is_loading = false;
+            vm.reportsPage = _.map(data, function (item) {
+                return reportPresentation(item);
+            });
+            vm.itemCount = headers['x-total-count'];
+            vm.itemsPerPage = headers['x-items-per-page'];
+        }, function () {
+            vm.is_loading = false;
+        });
+    }
+
+    vm.filterId = function (log) {
+        vm.searchParams.tags.push({
+            type: "request_id",
+            value: log.request_id
+        });
+    }
+    //initial load
+    var params = parseTagsToSearch(vm.searchParams);
+    vm.fetchReports(params);
+
+    $scope.$on('$locationChangeSuccess', function () {
+        console.log('$locationChangeSuccess ReportsListSlowController ');
+        vm.searchParams = parseSearchToTags($location.search());
+        var params = parseTagsToSearch(vm.searchParams);
+        console.log($location.path());
+        if (vm.is_loading === false) {
+            console.log(params);
+            vm.fetchReports(params);
+        }
+    });
+
+
+}
diff --git a/frontend/src/controllers/reports/view.js b/frontend/src/controllers/reports/view.js
new file mode 100644
index 0000000..4e1474d
--- /dev/null
+++ b/frontend/src/controllers/reports/view.js
@@ -0,0 +1,352 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers').controller('ReportsViewController', ReportsViewController);
+ReportsViewController.$inject = ['$window', '$location', '$state', '$uibModal',
+    '$cookies', 'reportGroupPropertyResource', 'reportGroupResource',
+    'logsNoIdResource', 'AeUser'];
+
+function ReportsViewController($window, $location, $state, $uibModal, $cookies, reportGroupPropertyResource, reportGroupResource, logsNoIdResource, AeUser) {
+    var vm = this;
+    vm.window = $window;
+    vm.reportHistoryConfig = {
+        data: {
+            json: [],
+            xFormat: '%Y-%m-%dT%H:%M:%S'
+        },
+        color: {
+            pattern: ['#6baed6', '#e6550d', '#74c476', '#fdd0a2', '#8c564b']
+        },
+        axis: {
+            x: {
+                type: 'timeseries',
+                tick: {
+                    format: '%Y-%m-%d'
+                }
+            },
+            y: {
+                tick: {
+                    count: 5,
+                    format: d3.format('.2s')
+                }
+            }
+        },
+        subchart: {
+            show: true,
+            size: {
+                height: 20
+            }
+        },
+        size: {
+            height: 250
+        },
+        zoom: {
+            rescale: true
+        },
+        grid: {
+            x: {
+                show: true
+            },
+            y: {
+                show: true
+            }
+        },
+        tooltip: {
+            format: {
+                title: function (d) {
+                    return '' + d;
+                },
+                value: function (v) {
+                    return v
+                }
+            }
+        }
+    };
+    vm.mentionedPeople = [];
+    vm.reportHistoryData = {};
+    vm.textTraceback = true;
+    vm.rawTraceback = '';
+    vm.traceback = '';
+    vm.reportType = 'report';
+    vm.report = null;
+    vm.showLong = false;
+    vm.reportLogs = null;
+    vm.requestStats = null;
+    vm.comment = null;
+    vm.is_loading = {
+        report: true,
+        logs: true,
+        history: true
+    };
+
+    vm.searchMentionedPeople = function(term){
+        //vm.mentionedPeople = [];
+        var term = term.toLowerCase();
+        reportGroupPropertyResource.get({
+                groupId: vm.report.group_id,
+                key: 'assigned_users'
+            }, null,
+            function (data) {
+                var users = [];
+                _.each(data.assigned, function(u){
+                    users.push({label: u.user_name});
+                });
+                _.each(data.unassigned, function(u){
+                    users.push({label: u.user_name});
+                });
+
+                var result = _.filter(users, function(u){
+                   return u.label.toLowerCase().indexOf(term) !== -1;
+                });
+                vm.mentionedPeople = result;
+            });
+    };
+
+    vm.searchTag = function (tag, value) {
+        console.log(tag, value);
+        if (vm.report.report_type === 3) {
+            $location.url($state.href('report.list_slow'));
+        }
+        else {
+            $location.url($state.href('report.list'));
+        }
+        $location.search(tag, value);
+    };
+
+    vm.tabs = {
+        slow_calls:false,
+        request_details:false,
+        logs:false,
+        comments:false,
+        affected_users:false
+    };
+    if ($cookies.selectedReportTab) {
+        vm.tabs[$cookies.selectedReportTab] = true;
+    }
+    else{
+        $cookies.selectedReportTab = 'request_details';
+        vm.tabs.request_details = true;
+    }
+
+    vm.fetchLogs = function () {
+        if (!vm.report.request_id){
+            return
+        }
+        vm.is_loading.logs = true;
+        logsNoIdResource.query({request_id: vm.report.request_id},
+            function (data) {
+            vm.is_loading.logs = false;
+            vm.reportLogs = data;
+        }, function () {
+            vm.is_loading.logs = false;
+        });
+    };
+    vm.addComment = function () {
+        reportGroupPropertyResource.save({
+                groupId: vm.report.group_id,
+                key: 'comments'
+            }, {body: vm.comment},
+            function (data) {
+                vm.report.comments.push(data);
+            });
+        vm.comment = '';
+    };
+
+    vm.fetchReport = function () {
+        vm.is_loading.report = true;
+        reportGroupResource.get($state.params, function (data) {
+            vm.is_loading.report = false;
+            if (data.request) {
+                try {
+                    var to_sort = _.pairs(data.request);
+                    data.request = _.object(_.sortBy(to_sort, function (i) {
+                        return i[0]
+                    }));
+                }
+                catch (err) {
+                }
+            }
+            vm.report = data;
+            if (vm.report.req_stats) {
+                vm.requestStats = [];
+                _.each(_.pairs(vm.report.req_stats['percentages']), function (p) {
+                    vm.requestStats.push({
+                        name: p[0],
+                        value: vm.report.req_stats[p[0]].toFixed(3),
+                        percent: p[1],
+                        calls: vm.report.req_stats[p[0] + '_calls']
+                    })
+                });
+            }
+            vm.traceback = data.traceback;
+            _.each(vm.traceback, function (frame) {
+                if (frame.line) {
+                    vm.rawTraceback += 'File ' + frame.file + ' line ' + frame.line + ' in ' + frame.fn + ": \r\n";
+                }
+                vm.rawTraceback += '    ' + frame.cline + "\r\n";
+            });
+
+            if (AeUser.id){
+                vm.fetchHistory();
+            }
+
+            vm.selectedTab($cookies.selectedReportTab);
+
+        }, function (response) {
+            console.log(response);
+            if (response.status == 403) {
+                var uid = response.headers('x-appenlight-uid');
+                if (!uid) {
+                    window.location = '/register?came_from=' + encodeURIComponent(window.location);
+                }
+            }
+            vm.is_loading.report = false;
+        });
+    };
+
+    vm.selectedTab = function(tab_name){
+        $cookies.selectedReportTab = tab_name;
+        if (tab_name == 'logs' && vm.reportLogs === null) {
+            vm.fetchLogs();
+        }
+    };
+
+    vm.markFixed = function () {
+        reportGroupResource.update({
+                groupId: vm.report.group_id
+            }, {fixed: !vm.report.group.fixed},
+            function (data) {
+                vm.report.group.fixed = data.fixed;
+            });
+    };
+
+    vm.markPublic = function () {
+        reportGroupResource.update({
+                groupId: vm.report.group_id
+            }, {public: !vm.report.group.public},
+            function (data) {
+                vm.report.group.public = data.public;
+            });
+    };
+
+    vm.delete = function () {
+        reportGroupResource.delete({'groupId': vm.report.group_id},
+            function (data) {
+            $state.go('report.list');
+        })
+    };
+
+    vm.assignUsersModal = function (index) {
+        vm.opts = {
+            backdrop: 'static',
+            templateUrl: 'AssignReportCtrl.html',
+            controller: 'AssignReportCtrl as ctrl',
+            resolve: {
+                report: function () {
+                    return vm.report;
+                }
+            }
+        };
+        var modalInstance = $uibModal.open(vm.opts);
+        modalInstance.result.then(function (report) {
+
+        }, function () {
+            console.info('Modal dismissed at: ' + new Date());
+        });
+
+    };
+
+    vm.fetchHistory = function () {
+        reportGroupPropertyResource.query({
+            groupId: vm.report.group_id,
+            key: 'history'
+        }, function (data) {
+            vm.reportHistoryData = {
+                json: data,
+                keys: {
+                    x: 'x',
+                    value: ["reports"]
+                },
+                names: {
+                    reports: 'Reports history'
+                },
+                type: 'bar'
+            };
+            vm.is_loading.history = false;
+        });
+    };
+
+    vm.nextDetail = function () {
+        $state.go('report.view_detail', {
+            groupId: vm.report.group_id,
+            reportId: vm.report.group.next_report
+        });
+    };
+    vm.previousDetail = function () {
+        $state.go('report.view_detail', {
+            groupId: vm.report.group_id,
+            reportId: vm.report.group.previous_report
+        });
+    };
+
+    vm.runIntegration = function (integration_name) {
+        console.log(integration_name);
+        if (integration_name == 'bitbucket') {
+            var controller = 'BitbucketIntegrationCtrl as ctrl';
+            var template_url = 'templates/integrations/bitbucket.html';
+        }
+        else if (integration_name == 'github') {
+            var controller = 'GithubIntegrationCtrl as ctrl';
+            var template_url = 'templates/integrations/github.html';
+        }
+        else if (integration_name == 'jira') {
+            var controller = 'JiraIntegrationCtrl as ctrl';
+            var template_url = 'templates/integrations/jira.html';
+        }
+        else {
+            return false;
+        }
+
+        vm.opts = {
+            backdrop: 'static',
+            templateUrl: template_url,
+            controller: controller,
+            resolve: {
+                integrationName: function () {
+                    return integration_name
+                },
+                report: function () {
+                    return vm.report;
+                }
+            }
+        };
+        var modalInstance = $uibModal.open(vm.opts);
+        modalInstance.result.then(function (report) {
+
+        }, function () {
+            console.info('Modal dismissed at: ' + new Date());
+        });
+
+    };
+
+    // load report
+    vm.fetchReport();
+
+
+}
diff --git a/frontend/src/controllers/user/alert_channel_email.js b/frontend/src/controllers/user/alert_channel_email.js
new file mode 100644
index 0000000..d13d507
--- /dev/null
+++ b/frontend/src/controllers/user/alert_channel_email.js
@@ -0,0 +1,46 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers')
+    .controller('AlertChannelsEmailController', AlertChannelsEmailController)
+
+AlertChannelsEmailController.$inject = ['$state','userSelfPropertyResource'];
+
+function AlertChannelsEmailController($state, userSelfPropertyResource) {
+    console.debug('AlertChannelsEmailController');
+    var vm = this;
+    vm.loading = {email: false};
+    vm.form = {};
+
+    vm.createChannel = function () {
+        vm.loading.email = true;
+        console.log('createChannel');
+        userSelfPropertyResource.save({key: 'alert_channels'}, vm.form, function () {
+            //vm.loading.email = false;
+            //setServerValidation(vm.channelForm);
+            //vm.form = {};
+            $state.go('user.alert_channels.list');
+        }, function (response) {
+            if (response.status == 422) {
+                setServerValidation(vm.channelForm, response.data);
+            }
+            vm.loading.email = false;
+        });
+    }
+}
diff --git a/frontend/src/controllers/user/alert_channels.js b/frontend/src/controllers/user/alert_channels.js
new file mode 100644
index 0000000..7e93533
--- /dev/null
+++ b/frontend/src/controllers/user/alert_channels.js
@@ -0,0 +1,129 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers').controller('AlertChannelsController', AlertChannelsController);
+
+AlertChannelsController.$inject = ['userSelfPropertyResource', 'applicationsNoIdResource'];
+
+function AlertChannelsController(userSelfPropertyResource, applicationsNoIdResource) {
+    console.debug('AlertChannelsController');
+    var vm = this;
+    vm.loading = {channels: true, applications: true, actions:true};
+
+    vm.alertChannels = userSelfPropertyResource.query({key: 'alert_channels'},
+        function (data) {
+            vm.loading.channels = false;
+        });
+
+    vm.alertActions = userSelfPropertyResource.query({key: 'alert_actions'},
+        function (data) {
+            vm.loading.actions = false;
+        });
+
+    vm.applications = applicationsNoIdResource.query({permission: 'view'},
+        function (data) {
+            vm.loading.applications = false;
+        });
+
+    var allOps = {
+        'eq': 'Equal',
+        'ne': 'Not equal',
+        'ge': 'Greater or equal',
+        'gt': 'Greater than',
+        'le': 'Lesser or equal',
+        'lt': 'Lesser than',
+        'startswith': 'Starts with',
+        'endswith': 'Ends with',
+        'contains': 'Contains'
+    };
+
+    var fieldOps = {};
+    fieldOps['http_status'] = ['eq', 'ne', 'ge', 'le'];
+    fieldOps['group:priority'] = ['eq', 'ne', 'ge', 'le'];
+    fieldOps['duration'] = ['ge', 'le'];
+    fieldOps['url_domain'] = ['eq', 'ne', 'startswith', 'endswith',
+        'contains'];
+    fieldOps['url_path'] = ['eq', 'ne', 'startswith', 'endswith',
+        'contains'];
+    fieldOps['error'] = ['eq', 'ne', 'startswith', 'endswith',
+        'contains'];
+    fieldOps['tags:server_name'] = ['eq', 'ne', 'startswith', 'endswith',
+        'contains'];
+    fieldOps['group:occurences'] = ['eq', 'ne', 'ge', 'le'];
+
+    var possibleFields = {
+        '__AND__': 'All met (composite rule)',
+        '__OR__': 'One met (composite rule)',
+        'http_status': 'HTTP Status',
+        'duration': 'Request duration',
+        'group:priority': 'Group -> Priority',
+        'url_domain': 'Domain',
+        'url_path': 'URL Path',
+        'error': 'Error',
+        'tags:server_name': 'Tag -> Server name',
+        'group:occurences': 'Group -> Occurences'
+    };
+
+    vm.ruleDefinitions = {
+        fieldOps: fieldOps,
+        allOps: allOps,
+        possibleFields: possibleFields
+    };
+
+    vm.addAction = function (channel) {
+        console.log('test');
+        userSelfPropertyResource.save({key: 'alert_channels_rules'}, {}, function (data) {
+            vm.alertActions.push(data);
+        }, function (response) {
+            if (response.status == 422) {
+                console.log('scope', response);
+            }
+        });
+    };
+
+    vm.updateChannel = function (channel, subKey) {
+        var params = {
+            key: 'alert_channels',
+            channel_name: channel['channel_name'],
+            channel_value: channel['channel_value']
+        };
+        var toUpdate = {};
+        if (['daily_digest', 'send_alerts'].indexOf(subKey) !== -1) {
+            toUpdate[subKey] = !channel[subKey];
+        }
+        userSelfPropertyResource.update(params, toUpdate, function (data) {
+            _.extend(channel, data);
+        });
+    };
+
+    vm.removeChannel = function (channel) {
+        console.log(channel);
+        userSelfPropertyResource.delete({
+            key: 'alert_channels',
+            channel_name: channel.channel_name,
+            channel_value: channel.channel_value
+        }, function () {
+            vm.alertChannels = _.filter(vm.alertChannels, function(item){
+                return item != channel;
+            });
+        });
+
+    }
+
+}
diff --git a/frontend/src/controllers/user/auth_tokens.js b/frontend/src/controllers/user/auth_tokens.js
new file mode 100644
index 0000000..94d1563
--- /dev/null
+++ b/frontend/src/controllers/user/auth_tokens.js
@@ -0,0 +1,63 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers').controller('UserAuthTokensController', UserAuthTokensController);
+
+UserAuthTokensController.$inject = ['$filter', 'userSelfPropertyResource', 'AeConfig'];
+
+function UserAuthTokensController($filter, userSelfPropertyResource, AeConfig) {
+    console.debug('UserAuthTokensController');
+    var vm = this;
+    vm.loading = {tokens: true};
+
+    vm.expireOptions = AeConfig.timeOptions;
+
+    vm.tokens = userSelfPropertyResource.query({key: 'auth_tokens'},
+        function (data) {
+            vm.loading.tokens = false;
+        });
+
+    vm.addToken = function () {
+        vm.loading.tokens = true;
+        userSelfPropertyResource.save({key: 'auth_tokens'},
+            vm.form,
+            function (data) {
+                vm.loading.tokens = false;
+                setServerValidation(vm.TokenForm);
+                vm.form = {};
+                vm.tokens.push(data);
+            }, function (response) {
+                vm.loading.tokens = false;
+                if (response.status == 422) {
+                    setServerValidation(vm.TokenForm, response.data);
+                }
+            })
+    }
+
+    vm.removeToken = function (token) {
+        userSelfPropertyResource.delete({key: 'auth_tokens',
+            token:token.token},
+            function () {
+                var index = vm.tokens.indexOf(token);
+                if (index !== -1) {
+                    vm.tokens.splice(index, 1);
+                }
+            })
+    }
+}
diff --git a/frontend/src/controllers/user/identities.js b/frontend/src/controllers/user/identities.js
new file mode 100644
index 0000000..0e79e42
--- /dev/null
+++ b/frontend/src/controllers/user/identities.js
@@ -0,0 +1,54 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers')
+    .controller('UserIdentitiesController', UserIdentitiesController)
+
+UserIdentitiesController.$inject = ['userSelfPropertyResource'];
+
+function UserIdentitiesController(userSelfPropertyResource) {
+    console.debug('UserIdentitiesController');
+    var vm = this;
+    vm.loading = {identities: true};
+
+    vm.identities = userSelfPropertyResource.query(
+        {key: 'external_identities'},
+        function (data) {
+            vm.loading.identities = false;
+            console.log(vm.identities);
+        });
+
+    vm.removeProvider = function (provider) {
+        console.log('provider', provider);
+        userSelfPropertyResource.delete(
+            {
+                key: 'external_identities',
+                provider: provider.provider,
+                id: provider.id
+            },
+            function (status) {
+                if (status){
+                    vm.identities = _.filter(vm.identities, function (item) {
+                        return item != provider
+                    });
+                }
+
+            });
+    }
+}
diff --git a/frontend/src/controllers/user/password.js b/frontend/src/controllers/user/password.js
new file mode 100644
index 0000000..27861b0
--- /dev/null
+++ b/frontend/src/controllers/user/password.js
@@ -0,0 +1,47 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers')
+    .controller('UserPasswordController', UserPasswordController)
+
+UserPasswordController.$inject = ['userSelfPropertyResource'];
+
+function UserPasswordController(userSelfPropertyResource) {
+    console.debug('UserPasswordController');
+    var vm = this;
+    vm.loading = {password: false};
+    vm.form = {};
+
+    vm.updatePassword = function () {
+        vm.loading.password = true;
+        console.log('updatePassword');
+        userSelfPropertyResource.update({key: 'password'}, vm.form, function () {
+            vm.loading.password = false;
+            vm.form = {};
+            setServerValidation(vm.passwordForm);
+        }, function (response) {
+            if (response.status == 422) {
+                console.log('vm',vm);
+                setServerValidation(vm.passwordForm, response.data);
+                console.log(response.data);
+            }
+            vm.loading.password = false;
+        });
+    }
+}
diff --git a/frontend/src/controllers/user/profile.js b/frontend/src/controllers/user/profile.js
new file mode 100644
index 0000000..30c4631
--- /dev/null
+++ b/frontend/src/controllers/user/profile.js
@@ -0,0 +1,49 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.controllers')
+    .controller('UserProfileController', UserProfileController)
+
+UserProfileController.$inject = ['userSelfResource'];
+
+function UserProfileController(userSelfResource) {
+    console.debug('UserProfileController');
+    var vm = this;
+    vm.loading = {profile: true};
+
+    vm.user = userSelfResource.get(null, function (data) {
+        vm.loading.profile = false;
+        console.log('loaded profile');
+    });
+
+    vm.updateProfile = function () {
+        vm.loading.profile = true;
+
+        console.log('updateProfile');
+        vm.user.$update(null, function () {
+            vm.loading.profile = false;
+            setServerValidation(vm.profileForm);
+        }, function (response) {
+            if (response.status == 422) {
+                setServerValidation(vm.profileForm, response.data);
+            }
+            vm.loading.profile = false;
+        });
+    }
+}
diff --git a/frontend/src/directives/app_version.js b/frontend/src/directives/app_version.js
new file mode 100644
index 0000000..e59fec8
--- /dev/null
+++ b/frontend/src/directives/app_version.js
@@ -0,0 +1,25 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+angular.module('appenlight.directives.appVersion', []).
+    directive('appVersion', ['version', function (version) {
+        return function (scope, elm, attrs) {
+            elm.text(version);
+        };
+    }])
\ No newline at end of file
diff --git a/frontend/src/directives/c3js.js b/frontend/src/directives/c3js.js
new file mode 100644
index 0000000..5e26d2f
--- /dev/null
+++ b/frontend/src/directives/c3js.js
@@ -0,0 +1,127 @@
+// # Copyright (C) 2010-2016  RhodeCode GmbH
+// #
+// # This program is free software: you can redistribute it and/or modify
+// # it under the terms of the GNU Affero General Public License, version 3
+// # (only), as published by the Free Software Foundation.
+// #
+// # This program is distributed in the hope that it will be useful,
+// # but WITHOUT ANY WARRANTY; without even the implied warranty of
+// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// # GNU General Public License for more details.
+// #
+// # You should have received a copy of the GNU Affero General Public License
+// # along with this program.  If not, see .
+// #
+// # This program is dual-licensed. If you wish to learn more about the
+// # App Enlight Enterprise Edition, including its added features, Support
+// # services, and proprietary license terms, please see
+// # https://rhodecode.com/licenses/
+
+// This code is inspired by https://github.com/jettro/c3-angular-sample/tree/master/js
+// License is MIT
+
+
+angular.module('appenlight.directives.c3chart', [])
+    .controller('ChartCtrl', ['$scope', '$timeout', function ($scope, $timeout) {
+        $scope.chart = null;
+        this.showGraph = function () {
+            var config = angular.copy($scope.config);
+            var firstLoad = true;
+            config.bindto = "#" + $scope.domid;
+            var originalXTickCount = null;
+            if ($scope.data && $scope.config) {
+                if (!_.isEmpty($scope.data)) {
+                    _.extend(config.data, angular.copy($scope.data));
+                }
+                console.log('ChartCtrl.showGraph', config);
+                config.onresized = function () {
+                    if (this.currentWidth < 400){
+                        $scope.chart.internal.config.axis_x_tick_culling_max = 3;
+                    }
+                    else if (this.currentWidth < 600){
+                        $scope.chart.internal.config.axis_x_tick_culling_max = 5;
+                    }
+                    else{
+                        $scope.chart.internal.config.axis_x_tick_culling_max = originalXTickCount;
+                    }
+                    $scope.chart.flush();
+                };
+
+
+                $scope.chart = c3.generate(config);
+                originalXTickCount = $scope.chart.internal.config.axis_x_tick_culling_max;
+                $scope.chart.internal.config.onresized.call($scope.chart.internal);
+            }
+            console.log('should update', $scope.update);
+            if ($scope.update) {
+                console.log('reload driven');
+                $scope.$watch('data', function () {
+                    if (!firstLoad) {
+                        console.log('data updated', $scope.data);
+                        $scope.chart.load(angular.copy($scope.data), {unload: true});
+                        if (typeof $scope.data.groups != 'undefined') {
+                            console.log('add groups');
+                            $scope.chart.groups($scope.data.groups);
+                        }
+                        if (typeof $scope.data.names != 'undefined') {
+                            console.log('add names');
+                            $scope.chart.data.names($scope.data.names);
+                        }
+                        $scope.chart.flush();
+                    }
+                }, true);
+            }
+            $scope.$watch('config.regions', function (newValue, oldValue) {
+                if (newValue === oldValue) {
+                    return
+                }
+                if (typeof $scope.config.regions != 'undefined') {
+                    console.log('update regions', $scope.config.regions);
+                    $scope.chart.regions($scope.config.regions);
+                }
+            });
+
+            firstLoad = false;
+            $scope.$watch('resizetrigger', function (newValue, oldValue) {
+                if (newValue !== oldValue) {
+                    $timeout(function () {
+                        $scope.chart.resize();
+                        $scope.chart.internal.config.onresized.call($scope.chart.internal);
+                    });
+                }
+            });
+        };
+    }])
+    .directive('c3chart', function ($timeout) {
+        var chartLinker = function (scope, element, attrs, chartCtrl) {
+            // Trick to wait for all rendering of the DOM to be finished.
+            // then we can tell c3js to "connect" to our dom node
+            $timeout(function () {
+                chartCtrl.showGraph()
+            });
+
+            scope.$on("$destroy", function () {
+                    if (scope.chart !== null) {
+                        scope.chart = scope.chart.destroy();
+                        delete element;
+                        delete scope.chart;
+                        console.log('destroy called');
+                    }
+                }
+            );
+        };
+        return {
+            "restrict": "E",
+            "controller": "ChartCtrl",
+            "scope": {
+                "domid": "@domid",
+                "config": "=config",
+                "data": "=data",
+                "resizetrigger": "=resizetrigger",
+                "update": "=update"
+            },
+            "template": "
", + "replace": true, + "link": chartLinker + } + }); diff --git a/frontend/src/directives/confirm_validate.js b/frontend/src/directives/confirm_validate.js new file mode 100644 index 0000000..1016a3f --- /dev/null +++ b/frontend/src/directives/confirm_validate.js @@ -0,0 +1,36 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.confirmValidate', []). +directive('confirmValidate', [function () { + return { + restrict: 'A', + require: 'ngModel', + link: function ($scope, elem, attrs, ngModel) { + ngModel.$validators.confirm = function (modelValue, viewValue) { + var value = modelValue || viewValue; + console.log('validate', value.toLowerCase() == 'confirm'); + if (value.toLowerCase() == 'confirm') { + return true; + } + return false; + } + } + } +}]) \ No newline at end of file diff --git a/frontend/src/directives/focus.js b/frontend/src/directives/focus.js new file mode 100644 index 0000000..00d49ba --- /dev/null +++ b/frontend/src/directives/focus.js @@ -0,0 +1,26 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.focus', []).directive('focus', function () { + return function (scope, element, attrs) { + attrs.$observe('focus', function (newValue) { + newValue === 'true' && element[0].focus(); + }); + } +}); \ No newline at end of file diff --git a/frontend/src/directives/form_errors.js b/frontend/src/directives/form_errors.js new file mode 100644 index 0000000..4cd6715 --- /dev/null +++ b/frontend/src/directives/form_errors.js @@ -0,0 +1,28 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.formErrors', []). +directive('formErrors', function() { + return { + scope: { + errors: '=' + }, + template: '
{{ errorMessage }}
' + } +}) diff --git a/frontend/src/directives/human_format.js b/frontend/src/directives/human_format.js new file mode 100644 index 0000000..f72cfad --- /dev/null +++ b/frontend/src/directives/human_format.js @@ -0,0 +1,36 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.humanFormat', []). + directive('humanFormat', [function () { + /* json inspector */ + return { + restrict: "A", + scope: { + vars: '=', + }, + "link": function (scope, element, attrs) { + scope.$watch('vars', function (newValue, oldValue, scope) { + element.empty(); + element.append(JsonHuman.format(scope.vars)); + }); + + } + } + }]) diff --git a/frontend/src/directives/iso_to_relative_time.js b/frontend/src/directives/iso_to_relative_time.js new file mode 100644 index 0000000..2337b83 --- /dev/null +++ b/frontend/src/directives/iso_to_relative_time.js @@ -0,0 +1,34 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.isoToRelativeTime', []). +directive('isoToRelativeTime', function () { + return { + "restrict": "E", + scope: { + time: '@' + }, + "link": function (scope, element) { + scope.$watch('time', function(newValue, oldValue, scope){ + element.empty(); + element.html(moment.utc(newValue).fromNow()); + }); + } + } +}) \ No newline at end of file diff --git a/frontend/src/directives/permissions.js b/frontend/src/directives/permissions.js new file mode 100644 index 0000000..bffe9b6 --- /dev/null +++ b/frontend/src/directives/permissions.js @@ -0,0 +1,217 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.controllers') + .controller('ApplicationPermissionsController', ApplicationPermissionsController); + +ApplicationPermissionsController.$inject = ['sectionViewResource', + 'applicationsPropertyResource', 'groupsResource'] + + +function ApplicationPermissionsController(sectionViewResource, applicationsPropertyResource , groupsResource) { + var vm = this; + vm.form = { + autocompleteUser: '', + selectedGroup: null, + selectedUserPermissions: {}, + selectedGroupPermissions: {} + } + vm.possibleGroups = groupsResource.query(null, function(){ + if (vm.possibleGroups.length > 0){ + vm.form.selectedGroup = vm.possibleGroups[0].id; + } + }); + console.log('g', vm.possibleGroups); + vm.possibleUsers = []; + _.each(vm.resource.possible_permissions, function (perm) { + vm.form.selectedUserPermissions[perm] = false; + vm.form.selectedGroupPermissions[perm] = false; + }); + + /** + * Converts the permission list into {user, permission_list objects} + * for rendering in templates + * **/ + var tmpObj = { + user: {}, + group: {} + }; + _.each(vm.currentPermissions, function (perm) { + console.log(perm); + if (perm.type == 'user') { + if (typeof tmpObj[perm.type][perm.user_name] === 'undefined') { + tmpObj[perm.type][perm.user_name] = { + self: perm, + permissions: [] + } + } + if (tmpObj[perm.type][perm.user_name].permissions.indexOf(perm.perm_name) === -1) { + tmpObj[perm.type][perm.user_name].permissions.push(perm.perm_name); + } + } + else { + if (typeof tmpObj[perm.type][perm.group_name] === 'undefined') { + tmpObj[perm.type][perm.group_name] = { + self: perm, + permissions: [] + } + } + if (tmpObj[perm.type][perm.group_name].permissions.indexOf(perm.perm_name) === -1) { + tmpObj[perm.type][perm.group_name].permissions.push(perm.perm_name); + } + + } + }); + vm.currentPermissions = { + user: _.values(tmpObj.user), + group: _.values(tmpObj.group), + }; + + console.log('test', tmpObj, vm.currentPermissions); + + vm.searchUsers = function (searchPhrase) { + console.log('SEARCHING'); + vm.searchingUsers = true; + return sectionViewResource.query({ + section: 'users_section', + view: 'search_users', + 'user_name': searchPhrase + }).$promise.then(function (data) { + vm.searchingUsers = false; + return _.map(data, function (item) { + return item; + }); + }); + }; + + + vm.setGroupPermission = function(){ + var POSTObj = { + 'group_id': vm.form.selectedGroup, + 'permissions': [] + }; + for (var key in vm.form.selectedGroupPermissions) { + if (vm.form.selectedGroupPermissions[key]) { + POSTObj.permissions.push(key) + } + } + applicationsPropertyResource.save({ + key: 'group_permissions', + resourceId: vm.resource.resource_id + }, POSTObj, + function (data) { + var found_row = false; + _.each(vm.currentPermissions.group, function (perm) { + if (perm.self.group_id == data.group.id) { + perm['permissions'] = data['permissions']; + found_row = true; + } + }); + if (!found_row) { + data.self = data.group; + // normalize data format + data.self.group_id = data.self.id; + vm.currentPermissions.group.push(data); + } + }); + + } + + + vm.setUserPermission = function () { + console.log('set permissions'); + var POSTObj = { + 'user_name': vm.form.autocompleteUser, + 'permissions': [] + }; + for (var key in vm.form.selectedUserPermissions) { + if (vm.form.selectedUserPermissions[key]) { + POSTObj.permissions.push(key) + } + } + applicationsPropertyResource.save({ + key: 'user_permissions', + resourceId: vm.resource.resource_id + }, POSTObj, + function (data) { + var found_row = false; + _.each(vm.currentPermissions.user, function (perm) { + if (perm.self.user_name == data['user_name']) { + perm['permissions'] = data['permissions']; + found_row = true; + } + }); + if (!found_row) { + data.self = data; + vm.currentPermissions.user.push(data); + } + }); + } + + vm.removeUserPermission = function (perm_name, curr_perm) { + console.log(perm_name); + console.log(curr_perm); + var POSTObj = { + key: 'user_permissions', + user_name: curr_perm.self.user_name, + permissions: [perm_name], + resourceId: vm.resource.resource_id + } + applicationsPropertyResource.delete(POSTObj, function (data) { + _.each(vm.currentPermissions.user, function (perm) { + if (perm.self.user_name == data['user_name']) { + perm['permissions'] = data['permissions'] + } + }); + }); + } + + vm.removeGroupPermission = function (perm_name, curr_perm) { + console.log('g', curr_perm); + var POSTObj = { + key: 'group_permissions', + group_id: curr_perm.self.group_id, + permissions: [perm_name], + resourceId: vm.resource.resource_id + } + applicationsPropertyResource.delete(POSTObj, function (data) { + _.each(vm.currentPermissions.group, function (perm) { + if (perm.self.group_id == data.group.id) { + perm['permissions'] = data['permissions'] + } + }); + }); + } +} + +angular.module('appenlight.directives.permissionsForm',[]) + .directive('permissionsForm', function () { + return { + "restrict": "E", + "controller": "ApplicationPermissionsController", + controllerAs: 'permissions', + bindToController: true, + scope: { + currentPermissions: '=', + possiblePermissions: '=', + resource: '=' + }, + templateUrl: 'templates/directives/permissions.html' + } + }) diff --git a/frontend/src/directives/plugin_config.js b/frontend/src/directives/plugin_config.js new file mode 100644 index 0000000..ba0c6dd --- /dev/null +++ b/frontend/src/directives/plugin_config.js @@ -0,0 +1,40 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.pluginConfig', []).directive('pluginConfig', function () { + return { + scope: {}, + bindToController: { + resource: '=', + section: '=' + }, + restrict: 'E', + templateUrl: 'templates/directives/plugin_config.html', + controller: PluginConfig, + controllerAs: 'plugin_ctrlr' + }; + + PluginConfig.$inject = ['stateHolder']; + + function PluginConfig(stateHolder) { + var vm = this; + vm.plugins = {}; + vm.inclusions = stateHolder.plugins.inclusions[vm.section]; + } +}); diff --git a/frontend/src/directives/postprocess_action.js b/frontend/src/directives/postprocess_action.js new file mode 100644 index 0000000..8b34463 --- /dev/null +++ b/frontend/src/directives/postprocess_action.js @@ -0,0 +1,121 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.postProcessAction', []).directive('postProcessAction', ['applicationsPropertyResource', function (applicationsPropertyResource) { + return { + scope: {}, + bindToController:{ + action: '=', + resource: '=' + }, + controller:postProcessActionController, + controllerAs:'ctrl', + restrict: 'E', + templateUrl: 'templates/directives/postprocess_action.html' + }; + function postProcessActionController(){ + var vm = this; + console.log(vm); + var allOps = { + 'eq': 'Equal', + 'ne': 'Not equal', + 'ge': 'Greater or equal', + 'gt': 'Greater than', + 'le': 'Lesser or equal', + 'lt': 'Lesser than', + 'startswith': 'Starts with', + 'endswith': 'Ends with', + 'contains': 'Contains' + }; + + var fieldOps = {}; + fieldOps['http_status'] = ['eq', 'ne', 'ge', 'le']; + fieldOps['group:priority'] = ['eq', 'ne', 'ge', 'le']; + fieldOps['duration'] = ['ge', 'le']; + fieldOps['url_domain'] = ['eq', 'ne', 'startswith', 'endswith', + 'contains']; + fieldOps['url_path'] = ['eq', 'ne', 'startswith', 'endswith', + 'contains']; + fieldOps['error'] = ['eq', 'ne', 'startswith', 'endswith', + 'contains']; + fieldOps['tags:server_name'] = ['eq', 'ne', 'startswith', 'endswith', + 'contains']; + fieldOps['group:occurences'] = ['eq', 'ne', 'ge', 'le']; + + var possibleFields = { + '__AND__': 'All met (composite rule)', + '__OR__': 'One met (composite rule)', + 'http_status': 'HTTP Status', + 'duration': 'Request duration', + 'group:priority': 'Group -> Priority', + 'url_domain': 'Domain', + 'url_path': 'URL Path', + 'error': 'Error', + 'tags:server_name': 'Tag -> Server name', + 'group:occurences': 'Group -> Occurences' + }; + + vm.ruleDefinitions = { + fieldOps: fieldOps, + allOps: allOps, + possibleFields: possibleFields + }; + + vm.possibleActions = [ + ['1', 'Priority +1'], + ['-1', 'Priority -1'] + ]; + + vm.deleteAction = function (action) { + applicationsPropertyResource.remove({ + pkey: vm.action.pkey, + resourceId: vm.resource.resource_id, + key: 'postprocessing_rules' + }, function () { + vm.resource.postprocessing_rules.splice( + vm.resource.postprocessing_rules.indexOf(action), 1); + }); + }; + + + vm.saveAction = function () { + var params = { + 'pkey': vm.action.pkey, + 'resourceId': vm.resource.resource_id, + key: 'postprocessing_rules' + }; + applicationsPropertyResource.update(params, vm.action, + function (data) { + vm.action.dirty = false; + vm.errors = []; + }, function (response) { + if (response.status == 422) { + var errorDict = angular.fromJson(response.data); + vm.errors = _.values(errorDict); + } + }); + }; + + vm.setDirty = function() { + vm.action.dirty = true; + console.log('set dirty'); + }; + } + +}]); diff --git a/frontend/src/directives/recursive.js b/frontend/src/directives/recursive.js new file mode 100644 index 0000000..5b55cd5 --- /dev/null +++ b/frontend/src/directives/recursive.js @@ -0,0 +1,37 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.recursive', []).directive("recursive", function ($compile) { + return { + restrict: "EACM", + priority: 100000, + compile: function (tElement, tAttr) { + var contents = tElement.contents().remove(); + var compiledContents; + return function (scope, iElement, iAttr) { + if (!compiledContents) { + compiledContents = $compile(contents); + } + iElement.append(compiledContents(scope, function (clone) { + return clone; + })); + }; + } + }; +}); diff --git a/frontend/src/directives/report_alert_action.js b/frontend/src/directives/report_alert_action.js new file mode 100644 index 0000000..949911e --- /dev/null +++ b/frontend/src/directives/report_alert_action.js @@ -0,0 +1,117 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.reportAlertAction', []).directive('reportAlertAction', ['userSelfPropertyResource', function (userSelfPropertyResource) { + return { + scope: {}, + bindToController:{ + action: '=', + applications: '=', + possibleChannels: '=', + actions: '=', + ruleDefinitions: '=' + }, + controller:reportAlertActionController, + controllerAs:'ctrl', + restrict: 'E', + templateUrl: 'templates/directives/report_alert_action.html' + }; + function reportAlertActionController(){ + var vm = this; + vm.deleteAction = function (actions, action) { + var get = { + key: 'alert_channels_rules', + pkey: action.pkey + }; + userSelfPropertyResource.remove(get, function (data) { + actions.splice(actions.indexOf(action), 1); + }); + + }; + + vm.bindChannel = function(){ + var post = { + channel_pkey: vm.channelToBind.pkey, + action_pkey: vm.action.pkey + }; + console.log(post); + userSelfPropertyResource.save({key: 'alert_channels_actions_binds'}, post, + function (data) { + vm.action.channels = []; + vm.action.channels = data.channels; + }, function (response) { + if (response.status == 422) { + console.log('scope', response); + } + }); + }; + + vm.unBindChannel = function(channel){ + userSelfPropertyResource.delete({ + key: 'alert_channels_actions_binds', + channel_pkey: channel.pkey, + action_pkey: vm.action.pkey + }, + function (data) { + vm.action.channels = []; + vm.action.channels = data.channels; + }, function (response) { + if (response.status == 422) { + console.log('scope', response); + } + }); + }; + + vm.saveAction = function () { + var params = { + key: 'alert_channels_rules', + pkey: vm.action.pkey + }; + userSelfPropertyResource.update(params, vm.action, + function (data) { + vm.action.dirty = false; + vm.errors = []; + }, function (response) { + if (response.status == 422) { + var errorDict = angular.fromJson(response.data); + vm.errors = _.values(errorDict); + } + }); + }; + + vm.possibleNotifications = [ + ['always', 'Always'], + ['only_first', 'Only New'], + ]; + + vm.possibleChannels = _.filter(vm.possibleChannels, function(c){ + return c.supports_report_alerting } + ); + + if (vm.possibleChannels.length > 0){ + vm.channelToBind = vm.possibleChannels[0]; + } + + vm.setDirty = function() { + vm.action.dirty = true; + console.log('set dirty'); + }; + } + +}]); diff --git a/frontend/src/directives/rule.js b/frontend/src/directives/rule.js new file mode 100644 index 0000000..756073f --- /dev/null +++ b/frontend/src/directives/rule.js @@ -0,0 +1,85 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.rule', []).directive('rule', function () { + return { + scope: {}, + bindToController:{ + parentObj: '=', + rule: '=', + ruleDefinitions: '=', + parentRule: "=", + config: "=" + }, + restrict: 'E', + templateUrl: 'templates/directives/rule.html', + controller:RuleController, + controllerAs:'rule_ctrlr' + }; + function RuleController(){ + var vm = this; + + vm.rule.dirty = false; + vm.oldField = vm.rule.field; + + vm.add = function () { + vm.rule.rules.push( + {op: "eq", field: 'http_status', value: ""} + ); + vm.setDirty(); + }; + + vm.setDirty = function() { + vm.rule.dirty = true; + console.log('set dirty'); + if (vm.parentObj){ + console.log('p', vm.parentObj); + console.log('set parent dirty'); + vm.parentObj.dirty = true; + } + }; + + vm.fieldChange = function () { + var new_is_compound = ['__AND__', '__OR__'].indexOf(vm.rule.field) !== -1; + var old_was_compound = ['__AND__', '__OR__'].indexOf(vm.oldField) !== -1; + + if (!new_is_compound) { + vm.rule.op = vm.ruleDefinitions.fieldOps[vm.rule.field][0]; + } + if ((new_is_compound && !old_was_compound)) { + console.log('resetting config'); + delete vm.rule.value; + vm.rule.rules = []; + vm.add(); + } + else if (!new_is_compound && old_was_compound) { + console.log('resetting config'); + delete vm.rule.rules; + vm.rule.value = ''; + } + vm.oldField = vm.rule.field; + vm.setDirty(); + }; + + vm.deleteRule = function (parent, rule) { + parent.rules.splice(parent.rules.indexOf(rule), 1); + vm.setDirty(); + } + } +}); diff --git a/frontend/src/directives/rule_read_only.js b/frontend/src/directives/rule_read_only.js new file mode 100644 index 0000000..df7bb7b --- /dev/null +++ b/frontend/src/directives/rule_read_only.js @@ -0,0 +1,43 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.ruleReadOnly', []).directive('ruleReadOnly', ['userSelfPropertyResource', function (userSelfPropertyResource) { + return { + scope: {}, + bindToController:{ + parentObj: '=', + rule: '=', + ruleDefinitions: '=', + parentRule: "=", + config: "=" + }, + restrict: 'E', + templateUrl: 'templates/directives/rule_read_only.html', + controller:RuleController, + controllerAs:'rule_ctrlr' + } + function RuleController(){ + var vm = this; + vm.readOnlyPossibleFields = {}; + var labelPairs = _.pairs(vm.parentObj.config); + _.each(labelPairs, function (entry) { + vm.readOnlyPossibleFields[entry[0]] = entry[1].human_label; + }); + } +}]); diff --git a/frontend/src/directives/small_report_group_list.js b/frontend/src/directives/small_report_group_list.js new file mode 100644 index 0000000..0a3deee --- /dev/null +++ b/frontend/src/directives/small_report_group_list.js @@ -0,0 +1,30 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.smallReportGroupList',[]). +directive('smallReportGroupList', [function () { + return { + restrict: "A", + scope: { + groups: '=', + applications: '=' + }, + templateUrl: 'templates/reports/small_report_group_list.html' + } +}]) \ No newline at end of file diff --git a/frontend/src/directives/small_report_list.js b/frontend/src/directives/small_report_list.js new file mode 100644 index 0000000..82f6246 --- /dev/null +++ b/frontend/src/directives/small_report_list.js @@ -0,0 +1,30 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.directives.smallReportList', []). + directive('smallReportList', [function () { + return { + restrict: "A", + scope: { + reports: '=', + applications: '=' + }, + templateUrl: 'templates/reports/small_report_list.html' + } + }]) \ No newline at end of file diff --git a/frontend/src/filters/filters.js b/frontend/src/filters/filters.js new file mode 100644 index 0000000..103d792 --- /dev/null +++ b/frontend/src/filters/filters.js @@ -0,0 +1,108 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +'use strict'; + +/* Filters */ + +angular.module('appenlight.filters'). + filter('interpolate', ['version', function (version) { + return function (text) { + return String(text).replace(/\%VERSION\%/mg, version); + } + }]) + .filter('isoToRelativeTime', function () { + return function (input) { + return moment.utc(input).fromNow(); + } + }) + + .filter('round', function () { + return function (input, precision) { + return input.toFixed(precision) + } + }) + + .filter('numberToThousands', function () { + return function (input) { + if (input > 1000000) { + var i = input / 1000000; + return i.toFixed(1).toString() + 'M' + } + else if (input > 1000) { + var i = input / 1000; + return i.toFixed(1).toString() + 'k' + } + else { + return input; + } + } + }) + .filter('getOrdered', function () { + return function (input, filterOn) { + var ordered = {}; + for (var key in input) { + ordered[input[key][filterOn]] = input[key]; + } + return ordered; + }; + }) + .filter('objectToOrderedArray', function(){ + return function(items, field, reverse) { + var filtered = []; + angular.forEach(items, function(item) { + filtered.push(item); + }); + filtered.sort(function (a, b) { + return (a[field] > b[field] ? 1 : -1); + }); + if(reverse) filtered.reverse(); + return filtered; + }; + }) + .filter('apdexValue', function () { + return function (input) { + if (input.apdex >= 95) { + return 'satisfactory'; + } else if (input.apdex >= 80) { + return 'tolerating'; + } else { + return 'frustrating'; + } + }; + }) + .filter('truncate', function(){ + return function (text, length, end) { + if (isNaN(length)) + length = 10; + + if (end === undefined) + end = "..."; + + if (text.length <= length || text.length - end.length <= length) { + return text; + } + else { + return String(text).substring(0, length-end.length) + end; + } + + }; + }) + +; diff --git a/frontend/src/routes.js b/frontend/src/routes.js new file mode 100644 index 0000000..06c7082 --- /dev/null +++ b/frontend/src/routes.js @@ -0,0 +1,250 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight').config(['$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) { + + $urlRouterProvider.otherwise('/ui'); + + $stateProvider.state('logs', { + url: '/ui/logs?resource', + templateUrl: 'templates/logs.html', + controller: 'LogsController as logs' + }); + + $stateProvider.state('front_dashboard', { + url: '/ui', + templateUrl: 'templates/dashboard.html', + controller: 'IndexDashboardController as index' + }); + + $stateProvider.state('report', { + abstract: true, + url: '/ui/report', + templateUrl: 'templates/reports/parent_view.html' + }); + + $stateProvider.state('report.list', { + url: '?start_date&min_duration&max_duration&{view_name:any}&{server_name:any}&resource', + templateUrl: 'templates/reports/list.html', + controller: 'ReportsListController as reports_list' + }); + + $stateProvider.state('report.list_slow', { + url: '/list_slow?start_date&min_duration&max_duration&{view_name:any}&{server_name:any}&resource', + templateUrl: 'templates/reports/list_slow.html', + controller: 'ReportsListSlowController as reports_list' + }); + + $stateProvider.state('report.view_detail', { + url: '/:groupId/:reportId', + templateUrl: 'templates/reports/view.html', + controller: 'ReportsViewController as report' + }); + $stateProvider.state('report.view_group', { + url: '/:groupId', + templateUrl: 'templates/reports/view.html', + controller: 'ReportsViewController as report' + }); + $stateProvider.state('events', { + url: '/ui/events', + templateUrl: 'templates/events.html', + controller: 'EventsController as events' + }); + $stateProvider.state('admin', { + url: '/ui/admin', + templateUrl: 'templates/admin/parent_view.html' + }); + $stateProvider.state('admin.user', { + abstract: true, + url: '/user', + templateUrl: 'templates/admin/users/parent_view.html' + }); + $stateProvider.state('admin.user.list', { + url: '/list', + templateUrl: 'templates/admin/users/users_list.html', + controller: 'AdminUsersController as users' + }); + $stateProvider.state('admin.user.create', { + url: '/create', + templateUrl: 'templates/admin/users/users_create.html', + controller: 'AdminUsersCreateController as user' + }); + $stateProvider.state('admin.user.update', { + url: '/{userId}/update', + templateUrl: 'templates/admin/users/users_create.html', + controller: 'AdminUsersCreateController as user' + }); + + + $stateProvider.state('admin.group', { + abstract: true, + url: '/group', + templateUrl: 'templates/admin/groups/parent_view.html' + }); + $stateProvider.state('admin.group.list', { + url: '/list', + templateUrl: 'templates/admin/groups/groups_list.html', + controller: 'AdminGroupsController as groups' + }); + $stateProvider.state('admin.group.create', { + url: '/create', + templateUrl: 'templates/admin/groups/groups_create.html', + controller: 'AdminGroupsCreateController as group' + }); + $stateProvider.state('admin.group.update', { + url: '/{groupId}/update', + templateUrl: 'templates/admin/groups/groups_create.html', + controller: 'AdminGroupsCreateController as group' + }); + + $stateProvider.state('admin.application', { + abstract: true, + url: '/application', + templateUrl: 'templates/admin/users/parent_view.html' + }); + + $stateProvider.state('admin.application.list', { + url: '/list', + templateUrl: 'templates/admin/applications/applications_list.html', + controller: 'AdminApplicationsListController as applications' + }); + + $stateProvider.state('admin.partitions', { + url: '/partitions', + templateUrl: 'templates/admin/partitions.html', + controller: 'AdminPartitionsController as partitions' + }); + $stateProvider.state('admin.system', { + url: '/system', + templateUrl: 'templates/admin/system.html', + controller: 'AdminSystemController as system' + }); + + $stateProvider.state('admin.configs', { + abstract: true, + url: '/configs', + templateUrl: 'templates/admin/configs/parent_view.html' + }); + + $stateProvider.state('admin.configs.list', { + url: '', + templateUrl: 'templates/admin/configs/edit.html', + controller: 'ConfigsListController as configs' + }); + + $stateProvider.state('user', { + url: '/ui/user', + templateUrl: 'templates/user/parent_view.html' + }); + + $stateProvider.state('user.profile', { + abstract: true, + url: '/profile', + templateUrl: 'templates/user/profile.html' + }); + $stateProvider.state('user.profile.edit', { + url: '', + templateUrl: 'templates/user/profile_edit.html', + controller: 'UserProfileController as profile' + }); + + + $stateProvider.state('user.profile.password', { + url: '/password', + templateUrl: 'templates/user/profile_password.html', + controller: 'UserPasswordController as password' + }); + + $stateProvider.state('user.profile.identities', { + url: '/identities', + templateUrl: 'templates/user/profile_identities.html', + controller: 'UserIdentitiesController as identities' + }); + + $stateProvider.state('user.profile.auth_tokens', { + url: '/auth_tokens', + templateUrl: 'templates/user/auth_tokens.html', + controller: 'UserAuthTokensController as auth_tokens' + }); + + $stateProvider.state('user.alert_channels', { + abstract: true, + url: '/alert_channels', + templateUrl: 'templates/user/alert_channels.html' + }); + + $stateProvider.state('user.alert_channels.list', { + url: '', + templateUrl: 'templates/user/alert_channels_list.html', + controller: 'AlertChannelsController as channels' + }); + + $stateProvider.state('user.alert_channels.email', { + url: '/email', + templateUrl: 'templates/user/alert_channels_email.html', + controller: 'AlertChannelsEmailController as email' + }); + + $stateProvider.state('applications', { + abstract: true, + url: '/ui/applications', + templateUrl: 'templates/applications/parent_view.html' + }); + + $stateProvider.state('applications.list', { + url: '', + templateUrl: 'templates/applications/list.html', + controller: 'ApplicationsListController as applications' + }); + $stateProvider.state('applications.update', { + url: '/{resourceId}/update', + templateUrl: 'templates/applications/applications_update.html', + controller: 'ApplicationsUpdateController as application' + }); + + $stateProvider.state('applications.integrations', { + url: '/{resourceId}/integrations', + templateUrl: 'templates/applications/integrations.html', + controller: 'IntegrationsListController as integrations', + data: { + resource: null + } + }); + + $stateProvider.state('applications.purge_logs', { + url: '/purge_logs', + templateUrl: 'templates/applications/applications_purge_logs.html', + controller: 'ApplicationsPurgeLogsController as applications_purge' + }); + + $stateProvider.state('applications.integrations.edit', { + url: '/{integration}', + templateUrl: function ($stateParams) { + return 'templates/applications/integrations/' + $stateParams.integration + '.html' + }, + controller: 'IntegrationController as integration' + }); + + $stateProvider.state('tests', { + url: '/ui/tests', + templateUrl: 'templates/user/alert_channels_test.html', + controller: 'AlertChannelsTestController as test_action' + }); + +}]); diff --git a/frontend/src/services/chart_result_parser.js b/frontend/src/services/chart_result_parser.js new file mode 100644 index 0000000..ec62757 --- /dev/null +++ b/frontend/src/services/chart_result_parser.js @@ -0,0 +1,133 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.services.chartResultParser',[]).factory('chartResultParser', function () { + + function transform(data) { + + /** transform result to a format that is more friendly + * to c3js we don't want to export this way as default + * as TSV stuff is less readable overall + * + * we want format of: + * {x: [unix_timestamps], + * key1: [val,list], + * key2: [val,list]...} + * + * OR + * + * handle special case where we want pie/donut for + * aggregation with a single metric, we need to transform + * the data from: + * [y:list, categories:[cat1,cat2,...]] + * to + * [cat1: val, cat2:val...] format to render properly + */ + var chartC3Config = { + data: { + json: [], + type: 'bar' + }, + point: { + show: false + }, + tooltip: { + format: { + title: function (d) { + if (d) { + return '' + d; + } + return ''; + }, + value: function (value, ratio, id, index) { + return d3.round(value, 3); + } + } + }, + regions: data.rect_regions + }; + var labels = _.keys(data.system_labels); + var specialCases = ['pie', 'donut', 'gauge']; + if (labels.length === 1 && _.contains(specialCases, + data.chart_type.type)) { + var transformedData = {}; + + _.each(data.series, function (item) { + transformedData[item['key']] = item[labels[0]]; + }); + } + else { + var transformedData = {'key': []}; + + _.each(labels, function (label) { + transformedData[label] = []; + }); + + _.each(data.series, function (item) { + for (key in item) { + transformedData[key].push(item[key]) + } + }); + } + + + if (data.parent_agg.type === 'time_histogram') { + chartC3Config.axis = { + x: { + type: 'timeseries', + tick: { + format: '%Y-%m-%d' + } + } + }; + chartC3Config.data.xFormat = '%Y-%m-%dT%H:%M:%S'; + } + else if (data.categories) { + chartC3Config.axis = { + x: { + type: 'category', + categories: data.categories + } + }; + // we don't want to show key as label if it is being + // used as a category instead + if (data.categories) { + delete transformedData['key']; + } + } + + var human_labels = {}; + _.each(_.pairs(data.system_labels), function(entry){ + human_labels[entry[0]] = entry[1].human_label; + }); + var chartC3Data = { + json: transformedData, + names: human_labels, + groups: data.groups, + type: data.chart_type.type + }; + + if (data.parent_agg.type == 'time_histogram') { + chartC3Data.x = 'key'; + } + return {chartC3Data: chartC3Data, chartC3Config: chartC3Config} + } + + return transform +}); diff --git a/frontend/src/services/resources.js b/frontend/src/services/resources.js new file mode 100644 index 0000000..757ecfd --- /dev/null +++ b/frontend/src/services/resources.js @@ -0,0 +1,153 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +var DEFAULT_ACTIONS = { + 'get': {method: 'GET', timeout: 60000 * 2}, + 'save': {method: 'POST', timeout: 60000 * 2}, + 'query': {method: 'GET', isArray: true, timeout: 60000 * 2}, + 'remove': {method: 'DELETE', timeout: 30000}, + 'update': {method: 'PATCH', timeout: 30000}, + 'delete': {method: 'DELETE', timeout: 30000} +}; + +angular.module('appenlight.services.resources', []).factory('usersResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.users, {userId: '@id'}, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('userResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.user, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('usersPropertyResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.usersProperty, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('userSelfResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.userSelf, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('userSelfPropertyResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.userSelfProperty, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('logsNoIdResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.logsNoId, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('reportsResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.reports, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('slowReportsResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.slowReports, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('reportGroupResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.reportGroup, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('reportGroupPropertyResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.reportGroupProperty, null, angular.copy(DEFAULT_ACTIONS)); +}]); + + +angular.module('appenlight.services.resources').factory('reportResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.reports, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('analyticsResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.analyticsAction, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('reportsResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.reports, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('integrationResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.integrationAction, null, angular.copy(DEFAULT_ACTIONS)); +}]); + + +angular.module('appenlight.services.resources').factory('adminResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.adminAction, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('applicationsNoIdResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.applicationsNoId, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('applicationsPropertyResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.applicationsProperty, null, angular.copy(DEFAULT_ACTIONS)); +}]); +angular.module('appenlight.services.resources').factory('applicationsResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.applications, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('sectionViewResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.sectionView, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('groupsNoIdResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.groupsNoId, null, angular.copy(DEFAULT_ACTIONS)); +}]); + + +angular.module('appenlight.services.resources').factory('groupsResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.groups, {userId: '@id'}, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('groupsPropertyResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.groupsProperty, null, angular.copy(DEFAULT_ACTIONS)); +}]); + + +angular.module('appenlight.services.resources').factory('eventsNoIdResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.eventsNoId, null, angular.copy(DEFAULT_ACTIONS)); +}]); + + +angular.module('appenlight.services.resources').factory('eventsResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.events, {userId: '@id'}, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('eventsPropertyResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.eventsProperty, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('configsNoIdResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.configsNoId, null, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('configsResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.configs, { + key: '@key', + section: '@section' + }, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('pluginConfigsResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.pluginConfigs, { + id: '@id', + plugin_name: '@plugin_name' + }, angular.copy(DEFAULT_ACTIONS)); +}]); + +angular.module('appenlight.services.resources').factory('resourcesPropertyResource', ['$resource', 'AeConfig', function ($resource, AeConfig) { + return $resource(AeConfig.urls.resourceProperty, null, angular.copy(DEFAULT_ACTIONS)); +}]); diff --git a/frontend/src/services/state_holder.js b/frontend/src/services/state_holder.js new file mode 100644 index 0000000..26c8cf3 --- /dev/null +++ b/frontend/src/services/state_holder.js @@ -0,0 +1,91 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.services.stateHolder', []).factory('stateHolder', ['$timeout', 'AeConfig', function ($timeout, AeConfig) { + /** + * Holds some common stuff like flash messages, but important part is + * plugins property that is a registry that holds all information about + * loaded plugins, its mutated via .run() functions on inclusion + * @type {{list: Array, timeout: null, extend: flashMessages.extend, pop: flashMessages.pop, cancelTimeout: flashMessages.cancelTimeout, removeMessages: flashMessages.removeMessages}} + */ + var flashMessages = { + list: [], + timeout: null, + extend: function (values) { + console.log('pushing flash', this); + if (this.list.length > 2) { + this.list.splice(0, this.list.length - 2); + } + this.list.push.apply(this.list, values); + this.cancelTimeout(); + this.removeMessages(); + }, + pop: function () { + console.log('popping flash'); + this.list.pop(); + }, + cancelTimeout: function () { + if (this.timeout) { + $timeout.cancel(this.timeout); + } + }, + removeMessages: function () { + var self = this; + this.timeout = $timeout(function () { + while (self.list.length > 0) { + self.list.pop(); + } + }, 10000); + } + }; + flashMessages.closeAlert = angular.bind(flashMessages, function (index) { + this.list.splice(index, 1); + this.cancelTimeout(); + }); + /* add flash messages from template generated on non-xhr request level */ + try { + if (AeConfig.flashMessages.length > 0) { + flashMessages.list = AeConfig.flashMessages; + } + } + catch (exc) { + + } + + var Plugins = { + enabled: [], + configs: {}, + inclusions: {}, + addInclusion: function (name, inclusion) { + var self = this; + if (self.inclusions.hasOwnProperty(name) === false) { + self.inclusions[name] = []; + } + self.inclusions[name].push(inclusion); + } + } + + var stateHolder = { + section: 'settings', + resource: null, + plugins: Plugins, + flashMessages: flashMessages, + }; + return stateHolder; +}]); diff --git a/frontend/src/services/type_ahead_tag_helper.js b/frontend/src/services/type_ahead_tag_helper.js new file mode 100644 index 0000000..94062fd --- /dev/null +++ b/frontend/src/services/type_ahead_tag_helper.js @@ -0,0 +1,57 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.services.typeAheadTagHelper', []).factory('typeAheadTagHelper', function () { + var typeAheadTagHelper = {tags: []}; + typeAheadTagHelper.aheadFilter = function (item, viewValue) { + //dont show "deeper" autocomplete like level:foo with exception of application ones + var label_text = item.text || item; + if (label_text.charAt(label_text.length - 1) != ':' && viewValue.indexOf(':') === -1 && label_text.indexOf('resource:') !== 0) { + return false; + } + if (viewValue.length > 2) { + // with apps we need to do it differently + if (viewValue.toLowerCase().indexOf('resource:') == 0) { + viewValue = viewValue.split(':').pop(); + } + // check if tags match + if (label_text.toLowerCase().indexOf(viewValue.toLowerCase()) === -1) { + return false; + } + } + return true; + }; + typeAheadTagHelper.removeSearchTag = function (tag) { + console.log(typeAheadTagHelper.tags); + var indexValue = _.indexOf(typeAheadTagHelper.tags, tag); + typeAheadTagHelper.tags.splice(indexValue, 1); + + }; + typeAheadTagHelper.addSearchTag = function (tag) { + // do not allow dupes - angular will complain + var found = _.find(typeAheadTagHelper.tags, function (existingTag) { + return existingTag.type == tag.type && existingTag.value == tag.value + }); + if (!found) { + typeAheadTagHelper.tags.push(tag); + } + }; + + return typeAheadTagHelper; +}); diff --git a/frontend/src/services/uuid_provider.js b/frontend/src/services/uuid_provider.js new file mode 100644 index 0000000..50ac6f0 --- /dev/null +++ b/frontend/src/services/uuid_provider.js @@ -0,0 +1,32 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +angular.module('appenlight.services.UUIDProvider', []).factory('UUIDProvider', function () { + var provider = { + genUUID4: function () { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( + /[xy]/g, function (c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : r & 0x3 | 0x8; + return v.toString(16); + } + ); + } + }; + return provider; +}); \ No newline at end of file diff --git a/frontend/src/templates/admin/applications/applications_list.html b/frontend/src/templates/admin/applications/applications_list.html new file mode 100644 index 0000000..87f0a8a --- /dev/null +++ b/frontend/src/templates/admin/applications/applications_list.html @@ -0,0 +1,45 @@ + + +
+
+ + Currently active applications: {{applications.applications.length}} + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Application nameOwner UserOwner Group
{{resource.resource_name}}{{resource.owner_user_name}}{{resource.owner_group_name}} + +
+
+
+ +
diff --git a/frontend/src/templates/admin/configs/edit.html b/frontend/src/templates/admin/configs/edit.html new file mode 100644 index 0000000..4e17380 --- /dev/null +++ b/frontend/src/templates/admin/configs/edit.html @@ -0,0 +1,83 @@ + + +
+ + +
+
+

Plugin Configuration

+
+
+ + +
+
diff --git a/frontend/src/templates/admin/configs/parent_view.html b/frontend/src/templates/admin/configs/parent_view.html new file mode 100644 index 0000000..f182cdd --- /dev/null +++ b/frontend/src/templates/admin/configs/parent_view.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/frontend/src/templates/admin/groups/groups_create.html b/frontend/src/templates/admin/groups/groups_create.html new file mode 100644 index 0000000..a917660 --- /dev/null +++ b/frontend/src/templates/admin/groups/groups_create.html @@ -0,0 +1,156 @@ + + +
+ +
+
+
+
+ + +
+ +
+
+ +
+ + +
+ +
+
+ + +
+ +
+ +
+
+
+
+
+ + +
+
+

Permissions summary

+
+
+

Direct application permissions

+ +
    +
  • + {{ perm.self.resource_name }} + +
    + + {{ perm.self.owner ? 'Resource owner' : perm_name }} + + + + +
    +
  • +
+ +

Direct dashboard permissions

+ +
    +
  • + {{ perm.self.resource_name }} + +
    + {{ perm.self.owner ? 'Resource owner' : perm_name }} + + + + +
    +
  • +
+ +
+ +
+ + +
+
+

User list

+
+
+ +
+
+ +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
UsernameEmailStatusFirst NameLast NameLast login
{{user.user_name}}{{user.email}}{{user.first_name}}{{user.last_name}}{{user.last_login_date | isoToRelativeTime}} + + + + + +
+
+
+ +
+ + +
diff --git a/frontend/src/templates/admin/groups/groups_list.html b/frontend/src/templates/admin/groups/groups_list.html new file mode 100644 index 0000000..9d51d25 --- /dev/null +++ b/frontend/src/templates/admin/groups/groups_list.html @@ -0,0 +1,47 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Group nameDescriptionMember count
{{group.group_name}}{{group.description}}{{group.member_count}} + + + + + +
+
+
+ +
+ diff --git a/frontend/src/templates/admin/groups/parent_view.html b/frontend/src/templates/admin/groups/parent_view.html new file mode 100644 index 0000000..f182cdd --- /dev/null +++ b/frontend/src/templates/admin/groups/parent_view.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/frontend/src/templates/admin/parent_view.html b/frontend/src/templates/admin/parent_view.html new file mode 100644 index 0000000..fcd4b1d --- /dev/null +++ b/frontend/src/templates/admin/parent_view.html @@ -0,0 +1,29 @@ + + + +
diff --git a/frontend/src/templates/admin/partitions.html b/frontend/src/templates/admin/partitions.html new file mode 100644 index 0000000..2c7c6ae --- /dev/null +++ b/frontend/src/templates/admin/partitions.html @@ -0,0 +1,92 @@ + + +
+ +
+
+ DELETE Daily Partitions +
+ +
+ +
+ + + + Check All + +
+ + + + + + + + + + +
DateIndices
{{row[0]}} +
    +
  • + ES: {{partition.name}} +
  • +
  • + PG: {{partition.name}} +
  • +
+
+
+ +
+ +
+
+ DELETE Permanent Partitions +
+ +
+ + +
+ +
+ + + Check All +
+ +
+ + + + + + + + + + +
DateIndices
{{row[0]}} +
    +
  • + ES: {{partition.name}} +
  • +
  • + PG: {{partition.name}} +
  • +
+
+
+ +
+ +
diff --git a/frontend/src/templates/admin/system.html b/frontend/src/templates/admin/system.html new file mode 100644 index 0000000..399d162 --- /dev/null +++ b/frontend/src/templates/admin/system.html @@ -0,0 +1,156 @@ + + +
+
+
+
+
+

+ System Info +

+
+
+ +

System Load: + 1min: {{system.systemLoad[0]}}, 5min: {{system.systemLoad[1]}}, 15min: {{system.systemLoad[2]}} +

+

Awaiting tasks: +

    +
  • reports: {{system.queueStats.waiting_reports}}
  • +
  • logs: {{system.queueStats.waiting_logs}}
  • +
  • metrics: {{system.queueStats.waiting_metrics}}
  • +
  • other: {{system.queueStats.waiting_other}}
  • +
+

+

Queue stats from last minute: +

    +
  • Processed reports: {{system.queueStats.processed_reports}}
  • +
  • Processed logs: {{system.queueStats.processed_logs}}
  • +
  • Processed metrics: {{system.queueStats.processed_metrics}}
  • +
+

+ +

Disks: +

    +
  • + {{disk.device}} {{disk.free}}/{{disk.total}}, {{disk.percentage}}% used +
  • +
+

+ +

Process stats: +

    +
  • FD soft limits: {{system.selfInfo.fds.soft}}
  • +
  • FD hard limits: {{system.selfInfo.fds.hard}}
  • +
  • Memlock soft limits: {{system.selfInfo.memlock.soft}}
  • +
  • Memlock hard limits: {{system.selfInfo.memlock.hard}}
  • +
+

+ +
+
+
+
+
+
+ +
+
+ + + + + Postgresql Tables + + + + + + + + + + + + + + + +
Table nameSize
{{row.table_name}}{{row.size_human}}
+ +
+ + + + Elasticsearch Indices + + + + + + + + + + + + + + + +
Index nameSize
{{row.name}}{{row.size_human}}
+ +
+ + + + Processes + + + + + + + + + + + + + + + + + + + + + + + + +
OwnerPIDCPUMEMName
{{row.owner}}{{row.pid}}{{row.cpu}}{{row.mem_usage}} ({{row.mem_percentage}}%){{row.name}}
{{row.command}}
+ +
+ + + + Python packages + + + + + + + +
{{package.name}}{{package.version}}
+

+ +
+ +
+
+
+
+
+
diff --git a/frontend/src/templates/admin/users/parent_view.html b/frontend/src/templates/admin/users/parent_view.html new file mode 100644 index 0000000..f182cdd --- /dev/null +++ b/frontend/src/templates/admin/users/parent_view.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/frontend/src/templates/admin/users/users_create.html b/frontend/src/templates/admin/users/users_create.html new file mode 100644 index 0000000..aab3e27 --- /dev/null +++ b/frontend/src/templates/admin/users/users_create.html @@ -0,0 +1,139 @@ + + +
+ +
+
+ + + Re-login to user + + + +
+
+ + +
+ +
+
+ +
+ + +
+ + +

Generate password + (generated password: {{user.gen_pass}}) +

+ +
+
+ + +
+ + +
+ +
+
+ +
+ + +
+ +
+
+
+ + +
+ +
+
+ +
+ + +
+ +
+
+ +
+ +
+ +
+
+
+
+
+ + +
+
+

Permission Summary

+
+
+

Direct application permissions

+ +
    +
  • + {{ perm.self.resource_name }} +
    + + {{ perm.self.owner ? 'Resource owner' : perm_name }} + + + + +
    +
  • +
+ +

Direct dashboard permissions

+ +
    +
  • + {{ perm.self.resource_name }} +
    + + {{ perm.self.owner ? 'Resource owner' : perm_name }} + + + + +
    +
  • +
+ +
+ +
+ + +
diff --git a/frontend/src/templates/admin/users/users_list.html b/frontend/src/templates/admin/users/users_list.html new file mode 100644 index 0000000..af1a596 --- /dev/null +++ b/frontend/src/templates/admin/users/users_list.html @@ -0,0 +1,64 @@ + + +
+ +
+ +
+ {{users.activeUsers}} active out of {{users.users.length}} users +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
UsernameStatusFirst NameLast NameLast login
{{user.user_name}}{{user.email}}{{user.first_name}}{{user.last_name}}{{user.last_login_date | isoToRelativeTime}} + + + + + +
+
+
+ + +
+
diff --git a/frontend/src/templates/applications/applications_purge_logs.html b/frontend/src/templates/applications/applications_purge_logs.html new file mode 100644 index 0000000..b733d03 --- /dev/null +++ b/frontend/src/templates/applications/applications_purge_logs.html @@ -0,0 +1,39 @@ + + +
+
+
+
+ +
+
+ + +
+ +
+
+ +
+ + +
+ +
+
+ +
+ + +
+ +
+
+ +
+
+
+
diff --git a/frontend/src/templates/applications/applications_update.html b/frontend/src/templates/applications/applications_update.html new file mode 100644 index 0000000..9377a1a --- /dev/null +++ b/frontend/src/templates/applications/applications_update.html @@ -0,0 +1,440 @@ + + +
+ +
+
+
+ +
+
+ + + + + API keys + + +

PRIVATE API KEY:

+

+

{{ application.resource.api_key }}
+

+

PUBLIC API KEY (for javascript clients):

+

+

{{ application.resource.public_key }}
+

+

Your key will be used to identify to which application your data + belongs to please keep them private at all times.

+ +
+ + + + Regenerate API keys + +

Are you sure you want to regenerate API KEY for this application?

+

All client application keys will need to be updated.

+
+ +
+ + +
+
+
+
+
+
+

How to connect your application?

+

Visit our developer documentation for step-by-step integration instructions.

+
+

+ Django Logo + Pyramid Logo + Flask Logo + + Javascript Logo + Node.js + Ruby Logo + PHP Logo + + +

+
+
+ +
+ +
+
+ + + +
+ +
+ + +
+ +
+ + +
+ +

Required for Javascript error tracking (one line one domain, skip http:// part)

+
+ + +
+
+ + +
+ +
+ +
+
+ + +
+ +

Application requires to send at least this amount of error reports per minute to open alert

+
+
+
+ + + +
+ +

Application requires to send at least this amount of slow reports per minute to open alert

+
+
+
+ + +
+ +

Allow permanent storage of logs in separate DB partitions (only administrator can enable this feature)

+
+
+
+ + +
+ +
+
+
+
+
+ +
+
+

Plugins

+
+
+ + + + +
+
+ +
+
+

API Testing

+
+
+

Please be sure to add at least one email alert channel for your account.

+

This will enable App Enlight to send you notification emails about errors inside your application.

+

After this is done you can use this CURL commands to test APIs:

+

(Please note that the data like execution times is semi randomly generated)

+ + + + Log API + + +
+
+curl -H "Content-Type: application/json" -k {{AeConfig.urls.baseUrl}}api/logs?protocol_version=0.5\&api_key={{application.resource.api_key}} -d '
+    [
+      {
+      "log_level": "WARNING",
+      "message": "OMG ValueError happened",
+      "namespace": "some.namespace.indicator",
+      "request_id": "SOME_UUID",
+      "permanent": false,
+      "primary_key": "random_key",
+      "server": "some.server.hostname",
+      "date": "{{application.momentJs.utc().milliseconds(0).toISOString()}}",
+      "tags": [["tag1","value"], ["tag2", 5]]
+      },
+      {
+      "log_level": "ERROR",
+      "message": "OMG ValueError happened2",
+      "namespace": "some.namespace.indicator",
+      "request_id": "SOME_UUID",
+      "permanent": false,
+      "server": "some.server.hostname",
+      "date": "{{application.momentJs.utc().milliseconds(0).toISOString()}}"
+      }
+    ]'
+                    
+
+ +
+ + + + Report API + + +
+
+curl -H "Content-Type: application/json" -k {{AeConfig.urls.baseUrl}}api/reports?protocol_version=0.5\&api_key={{application.resource.api_key}} -d '
+    [{
+    "client": "your-client-name-python",
+    "language": "python",
+    "view_name": "views/foo:bar",
+    "server": "SERVERNAME/INSTANCENAME",
+    "priority": 5,
+    "error": "OMG ValueError happened",
+    "occurences":1,
+    "http_status": 500,
+    "tags": [["tag1","value"], ["tag2", 5]],
+    "username": "USER",
+    "url": "HTTP://SOMEURL",
+    "ip": "127.0.0.1",
+    "start_time": "{{application.momentJs.utc().milliseconds(0).toISOString()}}",
+    "end_time": "{{application.momentJs.utc().milliseconds(0).add(2, 'seconds').toISOString()}}",
+    "user_agent": "BROWSER_AGENT",
+    "extra": [["message","CUSTOM MESSAGE"], ["custom_value", "some payload"]],
+    "request_id": "SOME_UUID",
+    "request": {"REQUEST_METHOD": "GET",
+             "PATH_INFO": "/FOO/BAR",
+             "POST": {"FOO":"BAZ","XXX":"YYY"}
+             },
+    "slow_calls":[{
+                   "start": "{{application.momentJs.utc().milliseconds(0).toISOString()}}",
+                   "end": "{{application.momentJs.utc().milliseconds(0).add(1, 'seconds').toISOString()}}",
+                   "type": "sql",
+                   "subtype": "postgresql",
+                   "parameters": ["QPARAM1","QPARAM2","QPARAMX"],
+                   "statement": "QUERY"
+                   }],
+    "request_stats": {
+                    "main": 2.50779,
+                    "nosql": 0.01008,
+                    "nosql_calls": 17.0,
+                    "remote": 0.0,
+                    "remote_calls": 0.0,
+                    "sql": 1,
+                    "sql_calls": 1.0,
+                    "tmpl": 0.0,
+                    "tmpl_calls": 0.0,
+                    "custom": 0.0,
+                    "custom_calls": 0.0
+                },
+    "traceback": [
+                {"cline": "return foo_bar_baz(1,2,3)",
+                "file": "somedir/somefile.py",
+                "fn": "somefunction",
+                "line": 454,
+                "vars": [["a_list",
+                         ["1",2,"4","5",6]],
+                         ["b", {"1": "2", "ccc": "ddd", "1": "a"}],
+                         ["obj", "object object at 0x7f0030853dc0"]]
+                        },
+                        {"cline": "OMG ValueError happened",
+                        "file": "",
+                        "fn": "",
+                        "line": "",
+                        "vars": []}
+                        ]
+                        }]'
+                    
+
+ +
+ + + + + Metrics API + + +
+
+curl -H "Content-Type: application/json" -k {{AeConfig.urls.baseUrl}}api/general_metrics?protocol_version=0.5\&api_key={{application.resource.api_key}} -d '
+        [{
+        "namespace": "some.monitor",
+        "timestamp": "{{application.momentJs.utc().milliseconds(0).toISOString()}}",
+        "server_name": "server.name",
+        "tags": [["value1", 15.7], ["value2", 26]]}]'
+                    
+
+ +
+ + + + + Request Stats API + + +
+
+curl -H "Content-Type: application/json" -k {{AeConfig.urls.baseUrl}}api/request_stats?protocol_version=0.5\&api_key={{application.resource.api_key}} -d '
+        [{"server": "some.server.hostname",
+          "timestamp": "{{application.momentJs.utc().milliseconds(0).toISOString()}}",
+          "metrics": [["dir/module:func",
+               {"custom": 0.0,
+                "custom_calls": 0,
+                "main": 0.01664,
+                "nosql": 0.00061,
+                "nosql_calls": 23,
+                "remote": 0.0,
+                "remote_calls": 0,
+                "requests": 1,
+                "sql": 0.00105,
+                "sql_calls": 2,
+                "tmpl": 0.0,
+                "tmpl_calls": 0}],
+              ["SomeView.function",
+               {"custom": 0.0,
+                "custom_calls": 0,
+                "main": 0.647261,
+                "nosql": 0.306554,
+                "nosql_calls": 140,
+                "remote": 0.0,
+                "remote_calls": 0,
+                "requests": 28,
+                "sql": 0.0,
+                "sql_calls": 0,
+                "tmpl": 0.0,
+                "tmpl_calls": 0}]]
+                }]'
+                    
+
+ +
+ +
+ +
+
+ + + +
+
+

Postprocessing

+
+
+

This section allows you influence the rating of report groups - if rule is matched once its not executed anymore

+ +

+ Add rule +

+ + +
+
+ +
+
+

Administration

+
+
+

Transfer ownership

+

Please note that by transfering ownership you WILL lose access to the application data and new owner needs to give you access permission

+
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+ +
+ +
+
+
+
+ +
+ +

Remove application

+

This operation will wipe out all data from database - there is no undo.

+ +
+
+
+ + +
+ +
+
+
+ +
+ +
+
+
+
+
+
+
diff --git a/frontend/src/templates/applications/breadcrumbs.html b/frontend/src/templates/applications/breadcrumbs.html new file mode 100644 index 0000000..0d0208c --- /dev/null +++ b/frontend/src/templates/applications/breadcrumbs.html @@ -0,0 +1,7 @@ + diff --git a/frontend/src/templates/applications/integrations.html b/frontend/src/templates/applications/integrations.html new file mode 100644 index 0000000..f3d0d07 --- /dev/null +++ b/frontend/src/templates/applications/integrations.html @@ -0,0 +1,74 @@ + + + + + diff --git a/frontend/src/templates/applications/integrations/bitbucket.html b/frontend/src/templates/applications/integrations/bitbucket.html new file mode 100644 index 0000000..cc9c631 --- /dev/null +++ b/frontend/src/templates/applications/integrations/bitbucket.html @@ -0,0 +1,53 @@ + + +
+
+
+ +

Bitbucket Integration

+ +
+
+ + + +
+ + + + +
+
https://bitbucket.org/
+ +
/
+ +
+ +
+
+
+ + + +
+ + + Remove Integration + + +
+
+
+ +

Remember you first need to + + authorize your user account + with Bitbucket before we can send issues on your behalf.

+ +

Every user will have to authorize App Enlight to access Bitbucket to be able to post issues.

+ +
+
diff --git a/frontend/src/templates/applications/integrations/campfire.html b/frontend/src/templates/applications/integrations/campfire.html new file mode 100644 index 0000000..2694e99 --- /dev/null +++ b/frontend/src/templates/applications/integrations/campfire.html @@ -0,0 +1,71 @@ + + +
+
+
+

Campfire Integration

+ +
+ +
+ + +
+ + +
+
http://
+ +
.campfirenow.com
+
+
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +

+ Room ID list separated by comma +

+
+
+ + +
+ +
+
diff --git a/frontend/src/templates/applications/integrations/flowdock.html b/frontend/src/templates/applications/integrations/flowdock.html new file mode 100644 index 0000000..fb3c162 --- /dev/null +++ b/frontend/src/templates/applications/integrations/flowdock.html @@ -0,0 +1,57 @@ + + +
+
+
+ +

Flowdock Integration

+ +
+ +
+ + + +
+ + +
+ + +
+ +
+ + + + +
+ + +
+ +
+
diff --git a/frontend/src/templates/applications/integrations/github.html b/frontend/src/templates/applications/integrations/github.html new file mode 100644 index 0000000..52ab9c6 --- /dev/null +++ b/frontend/src/templates/applications/integrations/github.html @@ -0,0 +1,65 @@ + + +
+
+
+ +

Github Integration

+ +
+ + +
+ + + +
+ + + + +
+
https://api.github.com/
+ +
/
+ +
+ +
+
+ +
+ + + + + + + Remove Integration + + + +
+
+ +

Remember you first need to + + authorize your user account + with Github before we can send issues on your behalf.

+ +

Every user will have to authorize App Enlight to access Github to be able to post issues.

+ +
+
Private repository access
+
+

If you need access to private repositories profile page allows you to require token including private repository permissions.

+ +

Registration page OAuth does NOT give you token with private repository access permissions.

+
+
+ +
+
diff --git a/frontend/src/templates/applications/integrations/hipchat.html b/frontend/src/templates/applications/integrations/hipchat.html new file mode 100644 index 0000000..09f29cc --- /dev/null +++ b/frontend/src/templates/applications/integrations/hipchat.html @@ -0,0 +1,66 @@ + + +
+
+
+ +

Hipchat Integration

+ +
+ +
+ + +
+ + +
+
+ +
+ + + +
+ + + +

+ Room ID list separated by comma +

+
+ +
+ +
+ + +
+ +
+ +
+
diff --git a/frontend/src/templates/applications/integrations/jira.html b/frontend/src/templates/applications/integrations/jira.html new file mode 100644 index 0000000..17b9e3a --- /dev/null +++ b/frontend/src/templates/applications/integrations/jira.html @@ -0,0 +1,71 @@ + + +
+
+
+ +

Jira Integration

+ +
+ +
+ + +
+ + +
+
+
+ + +
+ + + +
+
+
+ + +
+ + +
+
+
+ + +
+ + +
+
+
+ +
+ + + + Remove Integration + + +
+
+ +
+ + +
+
diff --git a/frontend/src/templates/applications/integrations/slack.html b/frontend/src/templates/applications/integrations/slack.html new file mode 100644 index 0000000..b8cbf9a --- /dev/null +++ b/frontend/src/templates/applications/integrations/slack.html @@ -0,0 +1,54 @@ + + +
+
+
+ +

Slack Integration

+ +
+ +
+ + +
+ + +
+
+ +
+ + + +
+
+ +
+
diff --git a/frontend/src/templates/applications/integrations/webhooks.html b/frontend/src/templates/applications/integrations/webhooks.html new file mode 100644 index 0000000..5cfa793 --- /dev/null +++ b/frontend/src/templates/applications/integrations/webhooks.html @@ -0,0 +1,48 @@ + + +
+
+
+ +

Webhooks Integration

+ +
+
+ + +
+ + +
+
+
+ + +
+ + +
+ + +
+
+ + +
+ + + Remove Integration + + +
+
+
+
+
diff --git a/frontend/src/templates/applications/list.html b/frontend/src/templates/applications/list.html new file mode 100644 index 0000000..8074052 --- /dev/null +++ b/frontend/src/templates/applications/list.html @@ -0,0 +1,31 @@ + + +
+
+
+ +

You have to create a new application first.

+ +
+ + + + + + + + + + + + + + + + +
Resource NameDomainsOptions
{{application.resource_name}}{{application.domains}} + Update + Integrations +
+ +
diff --git a/frontend/src/templates/applications/parent_view.html b/frontend/src/templates/applications/parent_view.html new file mode 100644 index 0000000..e99c4d7 --- /dev/null +++ b/frontend/src/templates/applications/parent_view.html @@ -0,0 +1,8 @@ +
+ + +
+ +
diff --git a/frontend/src/templates/dashboard.html b/frontend/src/templates/dashboard.html new file mode 100644 index 0000000..a47bed8 --- /dev/null +++ b/frontend/src/templates/dashboard.html @@ -0,0 +1,349 @@ + + +
+
+ +
+ +
+ +
+ +
+ +
+
+
+
+
+ + + + + +
+ + + + + +
+
+
+ +

+ +

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+ +

+ Average requests per second from all servers +

+ +

+ Average response time from all servers +

+ +

+ Aggregated average time spent per request - broken to layers +

+ +

+ Aggregated reports sent by your application +

+ +

+ Aggregated slow reports sent by your application +

+
+
+
+
+ + + + + +
+ +
+
+ +
+
+

Newest errors (real-time) +

+ + + + + + + + +
+
+ +

No new reports

+ +
+
+
+
+ +
+ +
+
+

Request breakdown over {{ index.timeSpan.label }}

+
+
+

+ +

+ +
+
+
+
+
+
+
+ + {{view.view_name}} + {{view.view_name}} + +
+ + avg. response {{view.avg_response}}s in + {{view.requests|numberToThousands}} requests + + +    Latest reports: + {{$index+1}} + + +
+ +
+ +
+
+ + +
+
+ +
+ +
+ +
+
+ +
+
+

+ Report groups trending over {{ index.timeSpan.label }} +

+
+
+

+ +

+ +

+ No reports found +

+ +
+
+
+ +
+ +
+ + +
+
+

+ Most common slow calls over {{ index.timeSpan.label }} +

+
+
+ +
+ +
+ + + + + + + + +
+ {{call.occurences|numberToThousands}} + + {{call.statement}} +
+ {{call.statement_type}} + {{call.statement_subtype}} + {{call.total_duration/call.occurences|round:2}}s + + Latest reports: + {{$index+1}} + +
+ + +
+
+ + +
+ +
+
+
+
diff --git a/frontend/src/templates/directives/permissions.html b/frontend/src/templates/directives/permissions.html new file mode 100644 index 0000000..512f072 --- /dev/null +++ b/frontend/src/templates/directives/permissions.html @@ -0,0 +1,84 @@ +
+
+

Permissions

+
+
+

Here you can set permissions for others to access your app data.

+ +

For example you can let other staff member view or alter error reports.

+ +
+

Group permissions

+ + + +
+
+ +
+
+ + {{ permission }} + +
+
+ +
+
+ +
+ +

User permissions

+
+ +
+
+

First enter username or full email of person you want to give access to (the person needs to be already registered in App Enlight)

+ +
+
+ +
+
+ + {{ permission }} + +
+
+ +
+
+
+
+
diff --git a/frontend/src/templates/directives/plugin_config.html b/frontend/src/templates/directives/plugin_config.html new file mode 100644 index 0000000..5dac3b5 --- /dev/null +++ b/frontend/src/templates/directives/plugin_config.html @@ -0,0 +1,5 @@ +
+
Plugin: {{tmpl.name}}
+ +
+
diff --git a/frontend/src/templates/directives/postprocess_action.html b/frontend/src/templates/directives/postprocess_action.html new file mode 100644 index 0000000..ec1b047 --- /dev/null +++ b/frontend/src/templates/directives/postprocess_action.html @@ -0,0 +1,29 @@ +
+
+
+ + + + +
+ +
+ + +
+ +
+ +  Save changes + +
+
+

Meeting following criteria:

+ + {{ctrl.rule}} + +
+
diff --git a/frontend/src/templates/directives/report_alert_action.html b/frontend/src/templates/directives/report_alert_action.html new file mode 100644 index 0000000..f38d654 --- /dev/null +++ b/frontend/src/templates/directives/report_alert_action.html @@ -0,0 +1,56 @@ +
+
+
+ + + + +
+ +
+ + +
+
+ + + +  Save changes + +
+
+

Channels:

+
    +
  • + {{channel.channel_visible_value}} +
    + + + + +
    +
  • +
+ +
+ You need to create an alert channel before you can assign it to your rule. +
+ +
+
+

Meeting following criteria:

+ + +
+
diff --git a/frontend/src/templates/directives/rule.html b/frontend/src/templates/directives/rule.html new file mode 100644 index 0000000..e3a8a7d --- /dev/null +++ b/frontend/src/templates/directives/rule.html @@ -0,0 +1,45 @@ +
+ +
+ +
+ +
+ + + + + +
+ + +

Subrules

+
+
+
+ + + +
+
+
+ + Add rule + +
+
+ + + + +
+
diff --git a/frontend/src/templates/directives/rule_read_only.html b/frontend/src/templates/directives/rule_read_only.html new file mode 100644 index 0000000..205e88b --- /dev/null +++ b/frontend/src/templates/directives/rule_read_only.html @@ -0,0 +1,25 @@ +
+ + + {{rule_ctrlr.readOnlyPossibleFields[rule_ctrlr.rule.field]}} + + + + is {{rule_ctrlr.ruleDefinitions.allOps[rule_ctrlr.rule.op]}} {{rule_ctrlr.rule.value}} + + + +

Subrules

+
+ +
+
+ + + +
+
+
+ +
+
diff --git a/frontend/src/templates/directives/search_type_ahead.html b/frontend/src/templates/directives/search_type_ahead.html new file mode 100644 index 0000000..1a3d3c6 --- /dev/null +++ b/frontend/src/templates/directives/search_type_ahead.html @@ -0,0 +1,7 @@ + + {{match.model.tag}} + {{match.label}} + - {{match.model.example}} +
{{match.model.description}}
+ +
diff --git a/frontend/src/templates/directives/user_search_type_ahead.html b/frontend/src/templates/directives/user_search_type_ahead.html new file mode 100644 index 0000000..d8608aa --- /dev/null +++ b/frontend/src/templates/directives/user_search_type_ahead.html @@ -0,0 +1,4 @@ + + {{match.label}} - + {{match.model.name}} + diff --git a/frontend/src/templates/events.html b/frontend/src/templates/events.html new file mode 100644 index 0000000..af72c4c --- /dev/null +++ b/frontend/src/templates/events.html @@ -0,0 +1,43 @@ +
+
+ +

Event history

+ + + + + + + +
+ + + + + +

For {{ event.resource_name }}

+ +

{{ event.text }}

+ created: + + + | closed: + + +
+ + + + + + + + +
+
+
diff --git a/frontend/src/templates/integrations/bitbucket.html b/frontend/src/templates/integrations/bitbucket.html new file mode 100644 index 0000000..3a41788 --- /dev/null +++ b/frontend/src/templates/integrations/bitbucket.html @@ -0,0 +1,36 @@ + + + diff --git a/frontend/src/templates/integrations/github.html b/frontend/src/templates/integrations/github.html new file mode 100644 index 0000000..0922f76 --- /dev/null +++ b/frontend/src/templates/integrations/github.html @@ -0,0 +1,37 @@ + + + diff --git a/frontend/src/templates/integrations/jira.html b/frontend/src/templates/integrations/jira.html new file mode 100644 index 0000000..35aaf99 --- /dev/null +++ b/frontend/src/templates/integrations/jira.html @@ -0,0 +1,36 @@ + + + diff --git a/frontend/src/templates/loader.html b/frontend/src/templates/loader.html new file mode 100644 index 0000000..149779e --- /dev/null +++ b/frontend/src/templates/loader.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/frontend/src/templates/logs.html b/frontend/src/templates/logs.html new file mode 100644 index 0000000..ee12848 --- /dev/null +++ b/frontend/src/templates/logs.html @@ -0,0 +1,98 @@ + + +
+ +

+ Search params: + + {{tag.type}} + {{ tag.type == 'resource' ? logs.applications[tag.value].resource_name : tag.value }} + + + +

+ +

+ + + +

+
+ +
+
+ +
+ +
+ +

+ +
+ +
+ + +
+
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + +
Logs
ApplicationMessageWhen
+ + {{log.resource_name}} + + + level: {{log.log_level}} + + namespace: {{log.namespace}} + + {{tag}}: {{value}} +
+ {{log.message}} +
+
+ + + +
+ +
+ +
+ +
+ +
diff --git a/frontend/src/templates/quickstart.html b/frontend/src/templates/quickstart.html new file mode 100644 index 0000000..348b81f --- /dev/null +++ b/frontend/src/templates/quickstart.html @@ -0,0 +1,39 @@ +

App Enlight quickstart

+ +

+ 1 + For App Enlight to operate you need to + create app profile that allows + you to + obtain API key that one of the clients can use. +

+ +
+
+ +

+ 2 + It is a good idea to configure an + + email alert channel that you can use to receive + notifications about events that happen in your application. +

+ +

+ It can be the same email account you used to register withing App Enlight - + although we often recommend using separate errors@... account + designated for alert notifications. +

+ +
+
+ +

+ 3 + In order for your application to stream meaningful information you will need to + integrate a compatible client for your language of choice. +

+ +

Head over to + developers section for information on currently available + clients that you can plug into your software

diff --git a/frontend/src/templates/register.html b/frontend/src/templates/register.html new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/frontend/src/templates/register.html diff --git a/frontend/src/templates/reports/list.html b/frontend/src/templates/reports/list.html new file mode 100644 index 0000000..f523ab0 --- /dev/null +++ b/frontend/src/templates/reports/list.html @@ -0,0 +1,89 @@ + + +
+ +

+ Search params: + + {{tag.type}} + {{ tag.type == 'resource' ? reports_list.applications[tag.value].resource_name : tag.value }} + + + +

+ +
+
+ +
+
+ + +
+ +
+ +

+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
Reports
#ApplicationWhen Error
+ {{report.group.priority}} + + {{report.group.occurences|numberToThousands}} + + +
{{report.resource_name}}
+ @{{report.tags.server_name}}
+ + + {{report.group.last_timestamp.replace('T', ' ').slice(0,16)}} + {{report.error || 'Unknown Exception'}}
+ {{ report.tags.view_name || report.url_path}}
+
+ + +
+ +
+ +
diff --git a/frontend/src/templates/reports/list_slow.html b/frontend/src/templates/reports/list_slow.html new file mode 100644 index 0000000..0106a07 --- /dev/null +++ b/frontend/src/templates/reports/list_slow.html @@ -0,0 +1,95 @@ + + +
+ +

+ Search params: + + {{tag.type}} + {{ tag.type == 'resource' ? reports_list.applications[tag.value].resource_name : tag.value }} + + + +

+ +

+ +

+
+ +
+
+ + +
+ +
+ +

+ + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Slow Request Reports
#Avg. durationApplicationWhen Location
+ {{report.group.priority}} + + {{report.group.occurences|numberToThousands}} + + {{report.group.average_duration.toFixed(3)}}s +
{{report.resource_name}}
+ @{{report.tags.server_name}}
+ + + {{report.group.last_timestamp.replace('T', ' ').slice(0,16)}} + + {{ report.tags.view_name || report.url_path}}
+ +
+ +
+ +
+ +
diff --git a/frontend/src/templates/reports/parent_view.html b/frontend/src/templates/reports/parent_view.html new file mode 100644 index 0000000..f182cdd --- /dev/null +++ b/frontend/src/templates/reports/parent_view.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/frontend/src/templates/reports/small_report_group_list.html b/frontend/src/templates/reports/small_report_group_list.html new file mode 100644 index 0000000..038ae31 --- /dev/null +++ b/frontend/src/templates/reports/small_report_group_list.html @@ -0,0 +1,16 @@ + + + + + + +
{{ report_group.occurences|numberToThousands }} + + {{ report_group.error || "Slow Report"}} +
+ {{report_group.summed_duration/report_group.occurences|round:2}}s + {{ report_group.view_name || report_group.url_path}} +
+ @{{applications[report_group.resource_id].resource_name}}
+ {{report_group.last_timestamp | isoToRelativeTime}} +
diff --git a/frontend/src/templates/reports/small_report_list.html b/frontend/src/templates/reports/small_report_list.html new file mode 100644 index 0000000..a882385 --- /dev/null +++ b/frontend/src/templates/reports/small_report_list.html @@ -0,0 +1,16 @@ + + + + + + +
{{ report.group.occurences|numberToThousands }} + + {{ report.error || "Slow Report"}} +
+ {{report.group.summed_duration/report.group.occurences|round:2}}s + {{ report.view_name || report.url_path}} +
+ @{{applications[report.resource_id].resource_name}}
+ {{report.last_timestamp | isoToRelativeTime}} +
diff --git a/frontend/src/templates/reports/view.html b/frontend/src/templates/reports/view.html new file mode 100644 index 0000000..52c4e5c --- /dev/null +++ b/frontend/src/templates/reports/view.html @@ -0,0 +1,480 @@ + + + + + + +
+ OOPS something went wrong :( +
+ +
+ + + +
+
+ +
+
+ +

Report Information

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Occurences{{report.report.group.occurences}}
HTTP status{{report.report.http_status}}
Priority{{report.report.group.priority}}
Public URL +
+ +
+
URL{{report.report.url}}
Remote IP{{report.report.ip}}
User Agent{{report.report.user_agent}}
Message{{report.report.message}}
Duration + {{report.report.duration}}s +
First occured + +
Last occured + +
+ +
+

Performance stats

+ +
+ + {{stat.calls}} + {{stat.name}} calls + + Other + + + +
+
+
+
+ 0s +
+
+ {{report.report.duration.toFixed(3)}}s +
+
+
+
+
+ +

Tags

+ + + + + + +
Username/UIDView NameServer Name{{ tag }} + {{ value }}
+ +
+
+ + +
+
+
+

Report history

+ +
+
+ + +
+
+ +
+ +
+ + + {{report.report.start_time.replace('T', ' ')}} UTC + ID: {{report.report.request_id}} + +
+ +
+ +

{{report.report.error}}

+ +
+ +

Traceback

+ + + +
+
{{report.rawTraceback}}
+
+
+ +
+
+ + + + + + + + File {{frame.file || 'Unknown file'}}, + + + Module {{frame.module || 'Unknown module'}}, + + line {{frame.line || 'Unknown line'}} + + in {{frame.fn || 'Unknown function'}} + +
+
{{frame.cline || 'Unknown context'}}
+ +
+ + + + + +
{{ fvar[0] }} + +
+ +
+
+
+ + +
+ + + + + + Slow Calls + + +

Slow Calls

+ +
+
+
+ +
+ No slow calls reported +
+ +
+ + + + + Request details + + +

Extra

+
+

Request details

+
+ +
+ + + + Logs + + +
+ +
+

No logs found

+ + + + + + + + + + + + + + + + + +
Logs
MessageWhen
+ + level: {{log.log_level}} + + namespace: {{log.namespace}} + + {{tag}}: {{value}} +
+ {{log.message}} +
+
+ + + +
+ +
+ + + + + Comments + {{report.report.comments.length}} + + + +

Comments

+ +

No comments yet - be first to add one!

+ +
+

+ {{comment.user_name}} + +

+

{{comment.body}}

+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+ + + + Affected users + {{report.report.affected_users_count}} + + + +

50 most affected users ID's by this issue:

+
    +
  • + {{user.username}} {{user.count}} +
  • +
+ +
+ +
+ + +
+ +
+
+
diff --git a/frontend/src/templates/user/alert_channels.html b/frontend/src/templates/user/alert_channels.html new file mode 100644 index 0000000..be33d1c --- /dev/null +++ b/frontend/src/templates/user/alert_channels.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/templates/user/alert_channels_email.html b/frontend/src/templates/user/alert_channels_email.html new file mode 100644 index 0000000..80bea6d --- /dev/null +++ b/frontend/src/templates/user/alert_channels_email.html @@ -0,0 +1,30 @@ + + +
+ +
+
+
+

Adding email alert channel - after you authorize your email in the system we can send alerts directly to this mailbox.

+
+
+ + +
+ +
+
+
+ +
+ +
+
+
+
+
+
diff --git a/frontend/src/templates/user/alert_channels_list.html b/frontend/src/templates/user/alert_channels_list.html new file mode 100644 index 0000000..8598484 --- /dev/null +++ b/frontend/src/templates/user/alert_channels_list.html @@ -0,0 +1,63 @@ + + +
+ +
+
+
+

Report alert rules

+

+ Add top-level rule +

+ + + +
+
+ +
+
+

Alert channels

+ +

Here you can configure your alert channels.

+ +

An alert channel serves as means of delivery of notifications about important events that happen in your applications.

+ +
You can add more integrations that support different alert channels via application management panel.
+ + + + + + +
{{ channel.channel_visible_value }} + + + + + Alerts + + + Daily digests + + + + Remove + + + +
+ +
+
+ +
diff --git a/frontend/src/templates/user/auth_tokens.html b/frontend/src/templates/user/auth_tokens.html new file mode 100644 index 0000000..977ff9c --- /dev/null +++ b/frontend/src/templates/user/auth_tokens.html @@ -0,0 +1,83 @@ + + +
+ +
+
+ +
+ +
You can use those tokens to authenticate yourself when performing various API calls
+ +
+ +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Your current tokens
DescriptionCreatedExpires

{{token.description}}

+
{{token.token| limitTo:token.limit}}...
+
{{token.creation_date | isoToRelativeTime}}{{token.expires | isoToRelativeTime}} + Never + + + + +
+
+ +
diff --git a/frontend/src/templates/user/breadcrumbs.html b/frontend/src/templates/user/breadcrumbs.html new file mode 100644 index 0000000..33cd308 --- /dev/null +++ b/frontend/src/templates/user/breadcrumbs.html @@ -0,0 +1,12 @@ + + + diff --git a/frontend/src/templates/user/index.html b/frontend/src/templates/user/index.html new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/frontend/src/templates/user/index.html diff --git a/frontend/src/templates/user/menu.html b/frontend/src/templates/user/menu.html new file mode 100644 index 0000000..aac76b7 --- /dev/null +++ b/frontend/src/templates/user/menu.html @@ -0,0 +1,27 @@ + + + + + +
+
Notifications
+ +
\ No newline at end of file diff --git a/frontend/src/templates/user/parent_view.html b/frontend/src/templates/user/parent_view.html new file mode 100644 index 0000000..d7936f6 --- /dev/null +++ b/frontend/src/templates/user/parent_view.html @@ -0,0 +1,7 @@ +
+ + +
+
diff --git a/frontend/src/templates/user/profile.html b/frontend/src/templates/user/profile.html new file mode 100644 index 0000000..be33d1c --- /dev/null +++ b/frontend/src/templates/user/profile.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/templates/user/profile_edit.html b/frontend/src/templates/user/profile_edit.html new file mode 100644 index 0000000..f037147 --- /dev/null +++ b/frontend/src/templates/user/profile_edit.html @@ -0,0 +1,92 @@ + + +
+
+
+
+
+
+ + +
+ +
+
+ +
+ + +
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+ +
+ +
+
+
+
+
+
diff --git a/frontend/src/templates/user/profile_identities.html b/frontend/src/templates/user/profile_identities.html new file mode 100644 index 0000000..16bce81 --- /dev/null +++ b/frontend/src/templates/user/profile_identities.html @@ -0,0 +1,48 @@ + + +
+ +
+
+
+ +
+

No external providers linked yet

+
    +
  • +
    + + + + +
    + @{{ provider.provider }}: {{ provider.id }} +
  • +
+
+ +
+
+
diff --git a/frontend/src/templates/user/profile_password.html b/frontend/src/templates/user/profile_password.html new file mode 100644 index 0000000..be6c36d --- /dev/null +++ b/frontend/src/templates/user/profile_password.html @@ -0,0 +1,50 @@ + + +
+ +
+
+
+ +
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+ +
+ +
+
+
+ +
+
+
diff --git a/frontend/src/underscore.js b/frontend/src/underscore.js new file mode 100644 index 0000000..89d20f9 --- /dev/null +++ b/frontend/src/underscore.js @@ -0,0 +1,23 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +var underscore = angular.module('underscore', []); +underscore.factory('_', function () { + return window._; // assumes underscore has already been loaded on the page +}); diff --git a/frontend/src/user.js b/frontend/src/user.js new file mode 100644 index 0000000..727c580 --- /dev/null +++ b/frontend/src/user.js @@ -0,0 +1,91 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +var aeuser = angular.module('appenlight.user', []); +aeuser.factory('AeUser', ['AeConfig', function () { + var decodedAeUser = decodeEncodedJSON(window.AE.user); + console.log('decodedAeUser', decodedAeUser); + var AeUser = { + user_name: decodedAeUser.user_name || null, + id: decodedAeUser.id, + assigned_reports: decodedAeUser.assigned_reports || null, + latest_events: decodedAeUser.latest_events || null, + permissions: decodedAeUser.permissions || null, + groups: decodedAeUser.groups || null, + applications: [], + dashboards: [] + }; + console.log('AeUser', AeUser); + AeUser.applications_map = {}; + AeUser.dashboards_map = {}; + AeUser.addApplication = function (item) { + AeUser.applications.push(item); + AeUser.applications_map[item.resource_id] = item; + }; + AeUser.addDashboard = function (item) { + AeUser.dashboards.push(item); + AeUser.dashboards_map[item.resource_id] = item; + }; + + AeUser.removeApplicationById = function (applicationId) { + AeUser.applications = _.filter(AeUser.applications, function (item) { + return item.resource_id != applicationId; + }); + delete AeUser.applications_map[applicationId]; + }; + AeUser.removeDashboardById = function (dashboardId) { + AeUser.dashboards = _.filter(AeUser.dashboards, function (item) { + return item.resource_id != dashboardId; + }); + delete AeUser.dashboards_map[dashboardId]; + }; + + AeUser.hasAppPermission = function (perm_name) { + if (AeUser.permissions.indexOf('root_administration') !== -1) { + return true + } + return AeUser.permissions.indexOf(perm_name) !== -1; + }; + + AeUser.hasContextPermission = function (permName, ACLList) { + var hasPerm = false; + _.each(ACLList, function (ACL) { + // is this the right perm? + if (ACL.perm_name == permName || + ACL.perm_name == '__all_permissions__') { + // perm for this user or a group user belongs to + if (ACL.user_name === AeUser.user_name || + AeUser.groups.indexOf(ACL.group_name) !== -1) { + hasPerm = true + } + } + }); + console.log('AeUser.hasContextPermission', permName, hasPerm); + return hasPerm; + }; + + _.each(decodedAeUser.applications, function (item) { + AeUser.addApplication(item); + }); + _.each(decodedAeUser.dashboards, function (item) { + AeUser.addDashboard(item); + }); + + return AeUser; +}]); diff --git a/frontend/src/utils.js b/frontend/src/utils.js new file mode 100644 index 0000000..4073558 --- /dev/null +++ b/frontend/src/utils.js @@ -0,0 +1,129 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # App Enlight Enterprise Edition, including its added features, Support +// # services, and proprietary license terms, please see +// # https://rhodecode.com/licenses/ + +if (!String.prototype.trim) { + String.prototype.trim = function () { + return this.replace(/^\s+|\s+$/g, ''); + }; + + String.prototype.ltrim = function () { + return this.replace(/^\s+/, ''); + }; + + String.prototype.rtrim = function () { + return this.replace(/\s+$/, ''); + }; + + String.prototype.fulltrim = function () { + return this.replace(/(?:(?:^|\n)\s+|\s+(?:$|\n))/g, '').replace(/\s+/g, ' '); + }; +} + +function decodeEncodedJSON (input){ + try{ + var val = JSON.parse(input); + delete doc; + return val; + }catch(exc){ + console.error('decodeEncodedJSON:' + exc + ' input:' + input); + delete doc; + } +} + +function parseTagsToSearch(searchParams) { + var params = {}; + _.each(searchParams.tags, function (t) { + if (!_.has(params, t.type)) { + params[t.type] = []; + } + params[t.type].push(t.value); + }); + if (searchParams.page > 1){ + params.page = searchParams.page; + } + return params; +} + +function parseSearchToTags(search) { + var config = {page: 1, tags: [], type:''}; + _.each(_.pairs(search), function (obj) { + if (_.isArray(obj[1])) { + _.each(obj[1], function (obj2) { + config.tags.push({type: obj[0], value: obj2}); + }) + } else { + if (obj[0] == 'page') { + config.page = obj[1]; + } + else if (obj[0] == 'type') { + config.type = obj[1]; + } + else { + config.tags.push({type: obj[0], value: obj[1]}); + } + + } + }); + return config; +} + + +/* returns ISO date string from UTC now - timespan */ +function timeSpanToStartDate(timeSpan){ + var amount = Number(timeSpan.slice(0,-1)); + var unit = timeSpan.slice(-1); + return moment.utc().subtract(amount, unit).format(); +} + +/* Sets server validation messages on form using angular machinery + +* custom key holding actual error messages */ +function setServerValidation(form, errors){ + console.log('form', form); + if (typeof form.ae_validation === 'undefined'){ + form.ae_validation = {}; + console.log('create ae_validation key'); + } + for (var key in form.ae_validation){ + form.ae_validation[key] = []; + console.log('clear key:', key); + } + console.log('errors:',errors); + + for (var key in form){ + if (key[0] !== '$' && key !== 'ae_validation'){ + form[key].$setValidity('ae_validation', true); + } + } + if (typeof errors !== undefined){ + for (var key in errors){ + if (typeof form[key] !== 'undefined'){ + form[key].$setValidity('ae_validation', false); + } + // handle wtforms and colander errors based on + // whether we have list of erors or a single error in a key + if (angular.isArray(errors[key])){ + form.ae_validation[key] = errors[key]; + } + else{ + form.ae_validation[key] = [errors[key]]; + } + } + } + return form; +} diff --git a/frontend/vendors/crel.js b/frontend/vendors/crel.js new file mode 100644 index 0000000..4686ba4 --- /dev/null +++ b/frontend/vendors/crel.js @@ -0,0 +1,123 @@ +//Copyright (C) 2012 Kory Nunn + +//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +//The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +/* + + This code is not formatted for readability, but rather run-speed and to assist compilers. + + However, the code's intention should be transparent. + + *** IE SUPPORT *** + + If you require this library to work in IE7, add the following after declaring crel. + + var testDiv = document.createElement('div'), + testLabel = document.createElement('label'); + + testDiv.setAttribute('class', 'a'); + testDiv['className'] !== 'a' ? crel.attrMap['class'] = 'className':undefined; + testDiv.setAttribute('name','a'); + testDiv['name'] !== 'a' ? crel.attrMap['name'] = function(element, value){ + element.id = value; + }:undefined; + + + testLabel.setAttribute('for', 'a'); + testLabel['htmlFor'] !== 'a' ? crel.attrMap['for'] = 'htmlFor':undefined; + + + +*/ + +(function (root, factory) { + if (typeof exports === 'object') { + if (!root.window) { + var jsdom = require('jsdom').jsdom; + root.window = jsdom().parentWindow; + } + module.exports = factory(root.window); + } else if (typeof define === 'function' && define.amd) { + define(factory.bind(null, window)); + } else { + root.crel = factory(root.window); + } +}(this, function (window) { + // based on http://stackoverflow.com/questions/384286/javascript-isdom-how-do-you-check-if-a-javascript-object-is-a-dom-object + var isNode = typeof Node === 'object' + ? function (object) { return object instanceof Node } + : function (object) { + return object + && typeof object === 'object' + && typeof object.nodeType === 'number' + && typeof object.nodeName === 'string'; + }; + + function crel(){ + var document = window.document, + args = arguments, //Note: assigned to a variable to assist compilers. Saves about 40 bytes in closure compiler. Has negligable effect on performance. + element = document.createElement(args[0]), + child, + settings = args[1], + childIndex = 2, + argumentsLength = args.length, + attributeMap = crel.attrMap; + + // shortcut + if(argumentsLength === 1){ + return element; + } + + if(typeof settings !== 'object' || isNode(settings)) { + --childIndex; + settings = null; + } + + // shortcut if there is only one child that is a string + if((argumentsLength - childIndex) === 1 && typeof args[childIndex] === 'string' && element.textContent !== undefined){ + element.textContent = args[childIndex]; + }else{ + for(; childIndex < argumentsLength; ++childIndex){ + child = args[childIndex]; + + if(child == null){ + continue; + } + + if(!isNode(child)){ + child = document.createTextNode(child); + } + + element.appendChild(child); + } + } + + for(var key in settings){ + if(!attributeMap[key]){ + element.setAttribute(key, settings[key]); + }else{ + var attr = crel.attrMap[key]; + if(typeof attr === 'function'){ + attr(element, settings[key]); + }else{ + element.setAttribute(attr, settings[key]); + } + } + } + + return element; + } + + // Used for mapping one kind of attribute to the supported version of that in bad browsers. + // String referenced so that compilers maintain the property name. + crel['attrMap'] = {}; + + // String referenced so that compilers maintain the property name. + crel["isNode"] = isNode; + + return crel; +})); diff --git a/frontend/vendors/reconnecting-websocket.js b/frontend/vendors/reconnecting-websocket.js new file mode 100644 index 0000000..a2c8974 --- /dev/null +++ b/frontend/vendors/reconnecting-websocket.js @@ -0,0 +1,179 @@ +// MIT License: +// +// Copyright (c) 2010-2012, Joe Walnes +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +/** + * This behaves like a WebSocket in every way, except if it fails to connect, + * or it gets disconnected, it will repeatedly poll until it succesfully connects + * again. + * + * It is API compatible, so when you have: + * ws = new WebSocket('ws://....'); + * you can replace with: + * ws = new ReconnectingWebSocket('ws://....'); + * + * The event stream will typically look like: + * onconnecting + * onopen + * onmessage + * onmessage + * onclose // lost connection + * onconnecting + * onopen // sometime later... + * onmessage + * onmessage + * etc... + * + * It is API compatible with the standard WebSocket API. + * + * Latest version: https://github.com/joewalnes/reconnecting-websocket/ + * - Joe Walnes + */ +function ReconnectingWebSocket(url, protocols) { + protocols = protocols || []; + + // These can be altered by calling code. + this.debug = false; + this.reconnectInterval = 1000; + this.timeoutInterval = 2000; + + var self = this; + var ws; + var forcedClose = false; + var timedOut = false; + + this.url = url; + this.protocols = protocols; + this.readyState = WebSocket.CONNECTING; + this.URL = url; // Public API + + this.onopen = function(event) { + }; + + this.onclose = function(event) { + }; + + this.onconnecting = function(event) { + }; + + this.onmessage = function(event) { + }; + + this.onerror = function(event) { + }; + + function connect(reconnectAttempt) { + ws = new WebSocket(url, protocols); + + self.onconnecting(); + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'attempt-connect', url); + } + + var localWs = ws; + var timeout = setTimeout(function() { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'connection-timeout', url); + } + timedOut = true; + localWs.close(); + timedOut = false; + }, self.timeoutInterval); + + ws.onopen = function(event) { + clearTimeout(timeout); + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'onopen', url); + } + self.readyState = WebSocket.OPEN; + reconnectAttempt = false; + self.onopen(event); + }; + + ws.onclose = function(event) { + clearTimeout(timeout); + ws = null; + if (forcedClose) { + self.readyState = WebSocket.CLOSED; + self.onclose(event); + } else { + self.readyState = WebSocket.CONNECTING; + self.onconnecting(); + if (!reconnectAttempt && !timedOut) { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'onclose', url); + } + self.onclose(event); + } + setTimeout(function() { + connect(true); + }, self.reconnectInterval); + } + }; + ws.onmessage = function(event) { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'onmessage', url, event.data); + } + self.onmessage(event); + }; + ws.onerror = function(event) { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'onerror', url, event); + } + self.onerror(event); + }; + } + connect(url); + + this.send = function(data) { + if (ws) { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'send', url, data); + } + return ws.send(data); + } else { + throw 'INVALID_STATE_ERR : Pausing to reconnect websocket'; + } + }; + + this.close = function() { + if (ws) { + forcedClose = true; + ws.close(); + } + }; + + /** + * Additional public API method to refresh the connection if still open (close, re-open). + * For example, if the app suspects bad data / missed heart beats, it can try to refresh. + */ + this.refresh = function() { + if (ws) { + ws.close(); + } + }; +} + +/** + * Setting this to true is the equivalent of setting all instances of ReconnectingWebSocket.debug to true. + */ +ReconnectingWebSocket.debugAll = false; + diff --git a/frontend/vendors/simple_moment_utc.js b/frontend/vendors/simple_moment_utc.js new file mode 100644 index 0000000..93e4318 --- /dev/null +++ b/frontend/vendors/simple_moment_utc.js @@ -0,0 +1 @@ +moment.defaultFormat = 'YYYY-MM-DDTHH:mm'; diff --git a/licenses/amqp_license.txt b/licenses/amqp_license.txt new file mode 100644 index 0000000..8f87b3a --- /dev/null +++ b/licenses/amqp_license.txt @@ -0,0 +1,458 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/licenses/bcrypt_license.txt b/licenses/bcrypt_license.txt new file mode 100644 index 0000000..11069ed --- /dev/null +++ b/licenses/bcrypt_license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "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/licenses/elasticsearch_license.txt b/licenses/elasticsearch_license.txt new file mode 100644 index 0000000..68c771a --- /dev/null +++ b/licenses/elasticsearch_license.txt @@ -0,0 +1,176 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "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. + diff --git a/licenses/gevent_websocket_license.txt b/licenses/gevent_websocket_license.txt new file mode 100644 index 0000000..3101606 --- /dev/null +++ b/licenses/gevent_websocket_license.txt @@ -0,0 +1,13 @@ + Copyright 2011-2013 Jeffrey Gelens + + 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/licenses/msgpack_license.txt b/licenses/msgpack_license.txt new file mode 100644 index 0000000..5f2280e --- /dev/null +++ b/licenses/msgpack_license.txt @@ -0,0 +1,13 @@ +Copyright (C) 2008-2011 INADA Naoki + + 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/licenses/python_editor.txt b/licenses/python_editor.txt new file mode 100644 index 0000000..e06d208 --- /dev/null +++ b/licenses/python_editor.txt @@ -0,0 +1,202 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "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/licenses/requests_license.txt b/licenses/requests_license.txt new file mode 100644 index 0000000..a103fc9 --- /dev/null +++ b/licenses/requests_license.txt @@ -0,0 +1,13 @@ +Copyright 2015 Kenneth Reitz + + 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/licenses/requests_toolbelt_license.txt b/licenses/requests_toolbelt_license.txt new file mode 100644 index 0000000..13c64ac --- /dev/null +++ b/licenses/requests_toolbelt_license.txt @@ -0,0 +1,13 @@ +Copyright 2014 Ian Cordasco, Cory Benfield + + 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/pytest.ini b/pytest.ini new file mode 100644 index 0000000..b2c732a --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = python/src/appenlight/tests +python_files = *.py diff --git a/readme.rst b/readme.rst new file mode 100644 index 0000000..85bc1ec --- /dev/null +++ b/readme.rst @@ -0,0 +1,63 @@ +App Enlight +----------- + +Automatic Installation +====================== + +Use the ansible scripts in the `/automation` directory to build complete instance of application +You can also use `packer` files in `/automation/packer` to create whole VM's for KVM and VMWare. + +Manual Installation +=================== + +Install the app by performing + + pip install -r requirements.txt + python setup.py develop + +To run the app and configure datastore you need to run: + +* elasticsearch (2.2+ tested) +* postgresql 9.5+ +* redis 2.8+ + +after installing the application you need to: + +1. (optional) generate production.ini (or use a copy of development.ini) + + appenlight-make-config production.ini + +2. setup database structure: + + appenlight-migrate-db -c FILENAME.ini + +3. to configure elasticsearch: + + appenlight-reindex-elasticsearch -c FILENAME.ini + +4. create base database objects + + appenlight-initializedb -c FILENAME.ini + +5. generate static assets + + appenlight-static -c FILENAME.ini + +Running application +=================== + +to run the main app: + + pserve development.ini + +to run celery workers: + + celery worker -A appenlight.celery -Q "reports,logs,metrics,default" --ini FILENAME.ini + +to run celery beat: + + celery beat -A appenlight.celery --ini FILENAME.ini + +to run appenlight's uptime plugin: + + appenlight-uptime-monitor -c FILENAME.ini diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..57ce6b3 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,27 @@ +[nosetests] +match=^test +nocapture=1 +cover-package=appenlight +with-coverage=1 +cover-erase=1 + +[compile_catalog] +directory = appenlight/locale +domain = appenlight +statistics = true + +[extract_messages] +add_comments = TRANSLATORS: +output_file = appenlight/locale/appenlight.pot +width = 80 + +[init_catalog] +domain = appenlight +input_file = appenlight/locale/appenlight.pot +output_dir = appenlight/locale + +[update_catalog] +domain = appenlight +input_file = appenlight/locale/appenlight.pot +output_dir = appenlight/locale +previous = true diff --git a/testing.ini b/testing.ini new file mode 100644 index 0000000..76b3b35 --- /dev/null +++ b/testing.ini @@ -0,0 +1,182 @@ +[app:appenlight] +use = egg:appenlight +reload_templates = true +debug_authorization = true +debug_notfound = true +debug_routematch = true +debug_templates = true +default_locale_name = en +sqlalchemy.url = postgresql://test:test@localhost/appenlight_test +sqlalchemy.pool_size = 2 +sqlalchemy.max_overflow = 5 +sqlalchemy.echo = false +jinja2.directories = appenlight:templates +jinja2.filters = nl2br = appenlight.lib.jinja2_filters.nl2br + + +pyramid.includes = pyramid_debugtoolbar + +appenlight.includes = + +# encryption +encryption_secret = oEOikr_T98wTh_xLH3w8Se3kmbgAQYSM4poZvPosya0= + +#redis +redis.url = redis://localhost:6379/0 +redis.redlock.url = redis://localhost:6379/3 + +#solr +elasticsearch.nodes = http://127.0.0.1:9200 + +#dirs +webassets.dir = %(here)s/webassets/ + +#authtkt +authtkt.secure = false +authtkt.secret = SECRET +# session settings +redis.sessions.secret = SECRET +redis.sessions.timeout = 3600 + +# session cookie settings +redis.sessions.cookie_name = appenlight +redis.sessions.cookie_max_age = 2592000 +redis.sessions.cookie_path = / +redis.sessions.cookie_domain = +redis.sessions.cookie_secure = False +redis.sessions.cookie_httponly = False +redis.sessions.cookie_on_exception = True +redis.sessions.prefix = appenlight:session: + + +#cache +cache.regions = default_term, second, short_term, long_term +cache.type = ext:memcached +cache.url = 127.0.0.1:11211 +cache.lock_dir = %(here)s/data/cache/lock +cache.second.expire = 1 +cache.short_term.expire = 60 +cache.default_term.expire = 300 + +#mailing +mailing.app_url = https://appenlight.com +mailing.from_name = App Enlight LOCAL +mailing.from_email = no-reply@status.appenlight.com + + +### +# Authomatic configuration +### + +authomatic.secret = secret +authomatic.pr.facebook.app_id = +authomatic.pr.facebook.secret = +authomatic.pr.twitter.key = +authomatic.pr.twitter.secret = +authomatic.pr.google.key = +authomatic.pr.google.secret = +authomatic.pr.github.key = +authomatic.pr.github.secret = +authomatic.pr.github.scope = repo, public_repo, user:email +authomatic.pr.bitbucket.key = +authomatic.pr.bitbucket.secret = + +#ziggurat +ziggurat_foundations.model_locations.User = appenlight.models.user:User +ziggurat_foundations.sign_in.username_key = sign_in_user_name +ziggurat_foundations.sign_in.password_key = sign_in_user_password +ziggurat_foundations.sign_in.came_from_key = came_from + +#cometd +cometd.server = http://127.0.0.1:8088/ +cometd.secret = secret +cometd.ws_url = wss://127.0.0.1:8088/ + + +# for celery +appenlight.api_key = +appenlight.transport_config = http://127.0.0.1:6543 + +celery.broker_type = redis +celery.broker_url = redis://localhost:6379/4 +celery.concurrency = 4 +celery.timezone = UTC +celery.always_eager = true + +[filter:paste_prefix] +use = egg:PasteDeploy#prefix + + +[filter:appenlight_client] +use = egg:appenlight_client +appenlight.api_key = +appenlight.transport_config = http://127.0.0.1:6543 +appenlight.report_local_vars = true +appenlight.report_404 = true +appenlight.timing.dbapi2_psycopg2 = 0.3 + + +[pipeline:main] +pipeline = + paste_prefix + appenlight_client + appenlight + + + +[server:main] +use = egg:waitress +host = 0.0.0.0 +port = 6543 + +[server:main_prod] +use = egg:gunicorn#main +host = 0.0.0.0:6543, unix:/tmp/appenlight.sock +workers = 6 +timeout = 90 +#max_requests = 1000 + + +# Begin logging configuration + +[loggers] +keys = root, appenlight, sqlalchemy, elasticsearch + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_appenlight] +level = INFO +handlers = +qualname = appenlight + +[logger_elasticsearch] +level = WARN +handlers = +qualname = elasticsearch + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/uptime_config.ini b/uptime_config.ini new file mode 100644 index 0000000..e5bba69 --- /dev/null +++ b/uptime_config.ini @@ -0,0 +1,5 @@ +[appenlight_uptime] +;sync_url = +;update_url = +location = 1 +api_key =