├── .gitignore ├── LICENSE ├── README.md ├── Rakefile ├── bin └── pencil ├── docs └── pencil_options.md ├── examples ├── dashboards.yml ├── graphs.yml └── pencil.yml ├── lib ├── pencil.rb └── pencil │ ├── config.rb │ ├── config.ru │ ├── helpers.rb │ ├── models.rb │ ├── models │ ├── base.rb │ ├── dashboard.rb │ ├── graph.rb │ └── host.rb │ ├── public │ ├── css │ │ └── jquery_themes │ │ │ └── pencil │ │ │ ├── images │ │ │ ├── ui-bg_flat_30_cccccc_40x100.png │ │ │ ├── ui-bg_flat_50_5c5c5c_40x100.png │ │ │ ├── ui-bg_glass_20_333333_1x400.png │ │ │ ├── ui-bg_glass_40_000000_1x400.png │ │ │ ├── ui-bg_glass_40_ffc73d_1x400.png │ │ │ ├── ui-bg_gloss-wave_25_333333_500x100.png │ │ │ ├── ui-bg_highlight-soft_15_000000_1x100.png │ │ │ ├── ui-bg_highlight-soft_80_eeeeee_1x100.png │ │ │ ├── ui-bg_inset-soft_30_ff4e0a_1x100.png │ │ │ ├── ui-icons_222222_256x240.png │ │ │ ├── ui-icons_4b8e0b_256x240.png │ │ │ ├── ui-icons_a83300_256x240.png │ │ │ ├── ui-icons_cccccc_256x240.png │ │ │ └── ui-icons_ffffff_256x240.png │ │ │ └── jquery-ui-1.8.16.custom.css │ ├── favicon.ico │ ├── js │ │ ├── cufon.js │ │ ├── gotham.font.js │ │ ├── jquery-1.6.2.min.js │ │ ├── jquery-ui-1.8.16.custom.min.js │ │ ├── pencil.js │ │ └── product-design.font.js │ └── style.css │ ├── rubyfixes.rb │ ├── version.rb │ └── views │ ├── cluster.erb │ ├── dash-cluster-zoom.erb │ ├── dash-cluster.erb │ ├── dash-global-zoom.erb │ ├── dash-global.erb │ ├── global.erb │ ├── host.erb │ ├── layout.erb │ └── partials │ ├── cluster_selector.erb │ ├── cluster_switcher.erb │ ├── dash_switcher.erb │ ├── graph_switcher.erb │ ├── hosts_selector.erb │ ├── input_boxes.erb │ └── shortcuts.erb └── pencil.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | .dired 2 | local 3 | *.gem 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MOZILLA PUBLIC LICENSE 2 | Version 1.1 3 | 4 | --------------- 5 | 6 | 1. Definitions. 7 | 8 | 1.0.1. "Commercial Use" means distribution or otherwise making the 9 | Covered Code available to a third party. 10 | 11 | 1.1. "Contributor" means each entity that creates or contributes to 12 | the creation of Modifications. 13 | 14 | 1.2. "Contributor Version" means the combination of the Original 15 | Code, prior Modifications used by a Contributor, and the Modifications 16 | made by that particular Contributor. 17 | 18 | 1.3. "Covered Code" means the Original Code or Modifications or the 19 | combination of the Original Code and Modifications, in each case 20 | including portions thereof. 21 | 22 | 1.4. "Electronic Distribution Mechanism" means a mechanism generally 23 | accepted in the software development community for the electronic 24 | transfer of data. 25 | 26 | 1.5. "Executable" means Covered Code in any form other than Source 27 | Code. 28 | 29 | 1.6. "Initial Developer" means the individual or entity identified 30 | as the Initial Developer in the Source Code notice required by Exhibit 31 | A. 32 | 33 | 1.7. "Larger Work" means a work which combines Covered Code or 34 | portions thereof with code not governed by the terms of this License. 35 | 36 | 1.8. "License" means this document. 37 | 38 | 1.8.1. "Licensable" means having the right to grant, to the maximum 39 | extent possible, whether at the time of the initial grant or 40 | subsequently acquired, any and all of the rights conveyed herein. 41 | 42 | 1.9. "Modifications" means any addition to or deletion from the 43 | substance or structure of either the Original Code or any previous 44 | Modifications. When Covered Code is released as a series of files, a 45 | Modification is: 46 | A. Any addition to or deletion from the contents of a file 47 | containing Original Code or previous Modifications. 48 | 49 | B. Any new file that contains any part of the Original Code or 50 | previous Modifications. 51 | 52 | 1.10. "Original Code" means Source Code of computer software code 53 | which is described in the Source Code notice required by Exhibit A as 54 | Original Code, and which, at the time of its release under this 55 | License is not already Covered Code governed by this License. 56 | 57 | 1.10.1. "Patent Claims" means any patent claim(s), now owned or 58 | hereafter acquired, including without limitation, method, process, 59 | and apparatus claims, in any patent Licensable by grantor. 60 | 61 | 1.11. "Source Code" means the preferred form of the Covered Code for 62 | making modifications to it, including all modules it contains, plus 63 | any associated interface definition files, scripts used to control 64 | compilation and installation of an Executable, or source code 65 | differential comparisons against either the Original Code or another 66 | well known, available Covered Code of the Contributor's choice. The 67 | Source Code can be in a compressed or archival form, provided the 68 | appropriate decompression or de-archiving software is widely available 69 | for no charge. 70 | 71 | 1.12. "You" (or "Your") means an individual or a legal entity 72 | exercising rights under, and complying with all of the terms of, this 73 | License or a future version of this License issued under Section 6.1. 74 | For legal entities, "You" includes any entity which controls, is 75 | controlled by, or is under common control with You. For purposes of 76 | this definition, "control" means (a) the power, direct or indirect, 77 | to cause the direction or management of such entity, whether by 78 | contract or otherwise, or (b) ownership of more than fifty percent 79 | (50%) of the outstanding shares or beneficial ownership of such 80 | entity. 81 | 82 | 2. Source Code License. 83 | 84 | 2.1. The Initial Developer Grant. 85 | The Initial Developer hereby grants You a world-wide, royalty-free, 86 | non-exclusive license, subject to third party intellectual property 87 | claims: 88 | (a) under intellectual property rights (other than patent or 89 | trademark) Licensable by Initial Developer to use, reproduce, 90 | modify, display, perform, sublicense and distribute the Original 91 | Code (or portions thereof) with or without Modifications, and/or 92 | as part of a Larger Work; and 93 | 94 | (b) under Patents Claims infringed by the making, using or 95 | selling of Original Code, to make, have made, use, practice, 96 | sell, and offer for sale, and/or otherwise dispose of the 97 | Original Code (or portions thereof). 98 | 99 | (c) the licenses granted in this Section 2.1(a) and (b) are 100 | effective on the date Initial Developer first distributes 101 | Original Code under the terms of this License. 102 | 103 | (d) Notwithstanding Section 2.1(b) above, no patent license is 104 | granted: 1) for code that You delete from the Original Code; 2) 105 | separate from the Original Code; or 3) for infringements caused 106 | by: i) the modification of the Original Code or ii) the 107 | combination of the Original Code with other software or devices. 108 | 109 | 2.2. Contributor Grant. 110 | Subject to third party intellectual property claims, each Contributor 111 | hereby grants You a world-wide, royalty-free, non-exclusive license 112 | 113 | (a) under intellectual property rights (other than patent or 114 | trademark) Licensable by Contributor, to use, reproduce, modify, 115 | display, perform, sublicense and distribute the Modifications 116 | created by such Contributor (or portions thereof) either on an 117 | unmodified basis, with other Modifications, as Covered Code 118 | and/or as part of a Larger Work; and 119 | 120 | (b) under Patent Claims infringed by the making, using, or 121 | selling of Modifications made by that Contributor either alone 122 | and/or in combination with its Contributor Version (or portions 123 | of such combination), to make, use, sell, offer for sale, have 124 | made, and/or otherwise dispose of: 1) Modifications made by that 125 | Contributor (or portions thereof); and 2) the combination of 126 | Modifications made by that Contributor with its Contributor 127 | Version (or portions of such combination). 128 | 129 | (c) the licenses granted in Sections 2.2(a) and 2.2(b) are 130 | effective on the date Contributor first makes Commercial Use of 131 | the Covered Code. 132 | 133 | (d) Notwithstanding Section 2.2(b) above, no patent license is 134 | granted: 1) for any code that Contributor has deleted from the 135 | Contributor Version; 2) separate from the Contributor Version; 136 | 3) for infringements caused by: i) third party modifications of 137 | Contributor Version or ii) the combination of Modifications made 138 | by that Contributor with other software (except as part of the 139 | Contributor Version) or other devices; or 4) under Patent Claims 140 | infringed by Covered Code in the absence of Modifications made by 141 | that Contributor. 142 | 143 | 3. Distribution Obligations. 144 | 145 | 3.1. Application of License. 146 | The Modifications which You create or to which You contribute are 147 | governed by the terms of this License, including without limitation 148 | Section 2.2. The Source Code version of Covered Code may be 149 | distributed only under the terms of this License or a future version 150 | of this License released under Section 6.1, and You must include a 151 | copy of this License with every copy of the Source Code You 152 | distribute. You may not offer or impose any terms on any Source Code 153 | version that alters or restricts the applicable version of this 154 | License or the recipients' rights hereunder. However, You may include 155 | an additional document offering the additional rights described in 156 | Section 3.5. 157 | 158 | 3.2. Availability of Source Code. 159 | Any Modification which You create or to which You contribute must be 160 | made available in Source Code form under the terms of this License 161 | either on the same media as an Executable version or via an accepted 162 | Electronic Distribution Mechanism to anyone to whom you made an 163 | Executable version available; and if made available via Electronic 164 | Distribution Mechanism, must remain available for at least twelve (12) 165 | months after the date it initially became available, or at least six 166 | (6) months after a subsequent version of that particular Modification 167 | has been made available to such recipients. You are responsible for 168 | ensuring that the Source Code version remains available even if the 169 | Electronic Distribution Mechanism is maintained by a third party. 170 | 171 | 3.3. Description of Modifications. 172 | You must cause all Covered Code to which You contribute to contain a 173 | file documenting the changes You made to create that Covered Code and 174 | the date of any change. You must include a prominent statement that 175 | the Modification is derived, directly or indirectly, from Original 176 | Code provided by the Initial Developer and including the name of the 177 | Initial Developer in (a) the Source Code, and (b) in any notice in an 178 | Executable version or related documentation in which You describe the 179 | origin or ownership of the Covered Code. 180 | 181 | 3.4. Intellectual Property Matters 182 | (a) Third Party Claims. 183 | If Contributor has knowledge that a license under a third party's 184 | intellectual property rights is required to exercise the rights 185 | granted by such Contributor under Sections 2.1 or 2.2, 186 | Contributor must include a text file with the Source Code 187 | distribution titled "LEGAL" which describes the claim and the 188 | party making the claim in sufficient detail that a recipient will 189 | know whom to contact. If Contributor obtains such knowledge after 190 | the Modification is made available as described in Section 3.2, 191 | Contributor shall promptly modify the LEGAL file in all copies 192 | Contributor makes available thereafter and shall take other steps 193 | (such as notifying appropriate mailing lists or newsgroups) 194 | reasonably calculated to inform those who received the Covered 195 | Code that new knowledge has been obtained. 196 | 197 | (b) Contributor APIs. 198 | If Contributor's Modifications include an application programming 199 | interface and Contributor has knowledge of patent licenses which 200 | are reasonably necessary to implement that API, Contributor must 201 | also include this information in the LEGAL file. 202 | 203 | (c) Representations. 204 | Contributor represents that, except as disclosed pursuant to 205 | Section 3.4(a) above, Contributor believes that Contributor's 206 | Modifications are Contributor's original creation(s) and/or 207 | Contributor has sufficient rights to grant the rights conveyed by 208 | this License. 209 | 210 | 3.5. Required Notices. 211 | You must duplicate the notice in Exhibit A in each file of the Source 212 | Code. If it is not possible to put such notice in a particular Source 213 | Code file due to its structure, then You must include such notice in a 214 | location (such as a relevant directory) where a user would be likely 215 | to look for such a notice. If You created one or more Modification(s) 216 | You may add your name as a Contributor to the notice described in 217 | Exhibit A. You must also duplicate this License in any documentation 218 | for the Source Code where You describe recipients' rights or ownership 219 | rights relating to Covered Code. You may choose to offer, and to 220 | charge a fee for, warranty, support, indemnity or liability 221 | obligations to one or more recipients of Covered Code. However, You 222 | may do so only on Your own behalf, and not on behalf of the Initial 223 | Developer or any Contributor. You must make it absolutely clear than 224 | any such warranty, support, indemnity or liability obligation is 225 | offered by You alone, and You hereby agree to indemnify the Initial 226 | Developer and every Contributor for any liability incurred by the 227 | Initial Developer or such Contributor as a result of warranty, 228 | support, indemnity or liability terms You offer. 229 | 230 | 3.6. Distribution of Executable Versions. 231 | You may distribute Covered Code in Executable form only if the 232 | requirements of Section 3.1-3.5 have been met for that Covered Code, 233 | and if You include a notice stating that the Source Code version of 234 | the Covered Code is available under the terms of this License, 235 | including a description of how and where You have fulfilled the 236 | obligations of Section 3.2. The notice must be conspicuously included 237 | in any notice in an Executable version, related documentation or 238 | collateral in which You describe recipients' rights relating to the 239 | Covered Code. You may distribute the Executable version of Covered 240 | Code or ownership rights under a license of Your choice, which may 241 | contain terms different from this License, provided that You are in 242 | compliance with the terms of this License and that the license for the 243 | Executable version does not attempt to limit or alter the recipient's 244 | rights in the Source Code version from the rights set forth in this 245 | License. If You distribute the Executable version under a different 246 | license You must make it absolutely clear that any terms which differ 247 | from this License are offered by You alone, not by the Initial 248 | Developer or any Contributor. You hereby agree to indemnify the 249 | Initial Developer and every Contributor for any liability incurred by 250 | the Initial Developer or such Contributor as a result of any such 251 | terms You offer. 252 | 253 | 3.7. Larger Works. 254 | You may create a Larger Work by combining Covered Code with other code 255 | not governed by the terms of this License and distribute the Larger 256 | Work as a single product. In such a case, You must make sure the 257 | requirements of this License are fulfilled for the Covered Code. 258 | 259 | 4. Inability to Comply Due to Statute or Regulation. 260 | 261 | If it is impossible for You to comply with any of the terms of this 262 | License with respect to some or all of the Covered Code due to 263 | statute, judicial order, or regulation then You must: (a) comply with 264 | the terms of this License to the maximum extent possible; and (b) 265 | describe the limitations and the code they affect. Such description 266 | must be included in the LEGAL file described in Section 3.4 and must 267 | be included with all distributions of the Source Code. Except to the 268 | extent prohibited by statute or regulation, such description must be 269 | sufficiently detailed for a recipient of ordinary skill to be able to 270 | understand it. 271 | 272 | 5. Application of this License. 273 | 274 | This License applies to code to which the Initial Developer has 275 | attached the notice in Exhibit A and to related Covered Code. 276 | 277 | 6. Versions of the License. 278 | 279 | 6.1. New Versions. 280 | Netscape Communications Corporation ("Netscape") may publish revised 281 | and/or new versions of the License from time to time. Each version 282 | will be given a distinguishing version number. 283 | 284 | 6.2. Effect of New Versions. 285 | Once Covered Code has been published under a particular version of the 286 | License, You may always continue to use it under the terms of that 287 | version. You may also choose to use such Covered Code under the terms 288 | of any subsequent version of the License published by Netscape. No one 289 | other than Netscape has the right to modify the terms applicable to 290 | Covered Code created under this License. 291 | 292 | 6.3. Derivative Works. 293 | If You create or use a modified version of this License (which you may 294 | only do in order to apply it to code which is not already Covered Code 295 | governed by this License), You must (a) rename Your license so that 296 | the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape", 297 | "MPL", "NPL" or any confusingly similar phrase do not appear in your 298 | license (except to note that your license differs from this License) 299 | and (b) otherwise make it clear that Your version of the license 300 | contains terms which differ from the Mozilla Public License and 301 | Netscape Public License. (Filling in the name of the Initial 302 | Developer, Original Code or Contributor in the notice described in 303 | Exhibit A shall not of themselves be deemed to be modifications of 304 | this License.) 305 | 306 | 7. DISCLAIMER OF WARRANTY. 307 | 308 | COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, 309 | WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, 310 | WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF 311 | DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. 312 | THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE 313 | IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, 314 | YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE 315 | COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER 316 | OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF 317 | ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. 318 | 319 | 8. TERMINATION. 320 | 321 | 8.1. This License and the rights granted hereunder will terminate 322 | automatically if You fail to comply with terms herein and fail to cure 323 | such breach within 30 days of becoming aware of the breach. All 324 | sublicenses to the Covered Code which are properly granted shall 325 | survive any termination of this License. Provisions which, by their 326 | nature, must remain in effect beyond the termination of this License 327 | shall survive. 328 | 329 | 8.2. If You initiate litigation by asserting a patent infringement 330 | claim (excluding declatory judgment actions) against Initial Developer 331 | or a Contributor (the Initial Developer or Contributor against whom 332 | You file such action is referred to as "Participant") alleging that: 333 | 334 | (a) such Participant's Contributor Version directly or indirectly 335 | infringes any patent, then any and all rights granted by such 336 | Participant to You under Sections 2.1 and/or 2.2 of this License 337 | shall, upon 60 days notice from Participant terminate prospectively, 338 | unless if within 60 days after receipt of notice You either: (i) 339 | agree in writing to pay Participant a mutually agreeable reasonable 340 | royalty for Your past and future use of Modifications made by such 341 | Participant, or (ii) withdraw Your litigation claim with respect to 342 | the Contributor Version against such Participant. If within 60 days 343 | of notice, a reasonable royalty and payment arrangement are not 344 | mutually agreed upon in writing by the parties or the litigation claim 345 | is not withdrawn, the rights granted by Participant to You under 346 | Sections 2.1 and/or 2.2 automatically terminate at the expiration of 347 | the 60 day notice period specified above. 348 | 349 | (b) any software, hardware, or device, other than such Participant's 350 | Contributor Version, directly or indirectly infringes any patent, then 351 | any rights granted to You by such Participant under Sections 2.1(b) 352 | and 2.2(b) are revoked effective as of the date You first made, used, 353 | sold, distributed, or had made, Modifications made by that 354 | Participant. 355 | 356 | 8.3. If You assert a patent infringement claim against Participant 357 | alleging that such Participant's Contributor Version directly or 358 | indirectly infringes any patent where such claim is resolved (such as 359 | by license or settlement) prior to the initiation of patent 360 | infringement litigation, then the reasonable value of the licenses 361 | granted by such Participant under Sections 2.1 or 2.2 shall be taken 362 | into account in determining the amount or value of any payment or 363 | license. 364 | 365 | 8.4. In the event of termination under Sections 8.1 or 8.2 above, 366 | all end user license agreements (excluding distributors and resellers) 367 | which have been validly granted by You or any distributor hereunder 368 | prior to termination shall survive termination. 369 | 370 | 9. LIMITATION OF LIABILITY. 371 | 372 | UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT 373 | (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL 374 | DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE, 375 | OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR 376 | ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY 377 | CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, 378 | WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER 379 | COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN 380 | INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF 381 | LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY 382 | RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW 383 | PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE 384 | EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO 385 | THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. 386 | 387 | 10. U.S. GOVERNMENT END USERS. 388 | 389 | The Covered Code is a "commercial item," as that term is defined in 390 | 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer 391 | software" and "commercial computer software documentation," as such 392 | terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 393 | C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), 394 | all U.S. Government End Users acquire Covered Code with only those 395 | rights set forth herein. 396 | 397 | 11. MISCELLANEOUS. 398 | 399 | This License represents the complete agreement concerning subject 400 | matter hereof. If any provision of this License is held to be 401 | unenforceable, such provision shall be reformed only to the extent 402 | necessary to make it enforceable. This License shall be governed by 403 | California law provisions (except to the extent applicable law, if 404 | any, provides otherwise), excluding its conflict-of-law provisions. 405 | With respect to disputes in which at least one party is a citizen of, 406 | or an entity chartered or registered to do business in the United 407 | States of America, any litigation relating to this License shall be 408 | subject to the jurisdiction of the Federal Courts of the Northern 409 | District of California, with venue lying in Santa Clara County, 410 | California, with the losing party responsible for costs, including 411 | without limitation, court costs and reasonable attorneys' fees and 412 | expenses. The application of the United Nations Convention on 413 | Contracts for the International Sale of Goods is expressly excluded. 414 | Any law or regulation which provides that the language of a contract 415 | shall be construed against the drafter shall not apply to this 416 | License. 417 | 418 | 12. RESPONSIBILITY FOR CLAIMS. 419 | 420 | As between Initial Developer and the Contributors, each party is 421 | responsible for claims and damages arising, directly or indirectly, 422 | out of its utilization of rights under this License and You agree to 423 | work with Initial Developer and Contributors to distribute such 424 | responsibility on an equitable basis. Nothing herein is intended or 425 | shall be deemed to constitute any admission of liability. 426 | 427 | 13. MULTIPLE-LICENSED CODE. 428 | 429 | Initial Developer may designate portions of the Covered Code as 430 | "Multiple-Licensed". "Multiple-Licensed" means that the Initial 431 | Developer permits you to utilize portions of the Covered Code under 432 | Your choice of the NPL or the alternative licenses, if any, specified 433 | by the Initial Developer in the file described in Exhibit A. 434 | 435 | EXHIBIT A -Mozilla Public License. 436 | 437 | ``The contents of this file are subject to the Mozilla Public License 438 | Version 1.1 (the "License"); you may not use this file except in 439 | compliance with the License. You may obtain a copy of the License at 440 | http://www.mozilla.org/MPL/ 441 | 442 | Software distributed under the License is distributed on an "AS IS" 443 | basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the 444 | License for the specific language governing rights and limitations 445 | under the License. 446 | 447 | The Original Code is ______________________________________. 448 | 449 | The Initial Developer of the Original Code is ________________________. 450 | Portions created by ______________________ are Copyright (C) ______ 451 | _______________________. All Rights Reserved. 452 | 453 | Contributor(s): ______________________________________. 454 | 455 | Alternatively, the contents of this file may be used under the terms 456 | of the _____ license (the "[___] License"), in which case the 457 | provisions of [______] License are applicable instead of those 458 | above. If you wish to allow use of your version of this file only 459 | under the terms of the [____] License and not to allow others to use 460 | your version of this file under the MPL, indicate your decision by 461 | deleting the provisions above and replace them with the notice and 462 | other provisions required by the [___] License. If you do not delete 463 | the provisions above, a recipient may use your version of this file 464 | under either the MPL or the [___] License." 465 | 466 | [NOTE: The text of this Exhibit A may differ slightly from the text of 467 | the notices in the Source Code files of the Original Code. You should 468 | use the text of this Exhibit A rather than the text found in the 469 | Original Code Source Code for Your Modifications.] 470 | 471 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pencil 2 | 3 | *NOTE* If you are checking out pencil for the first time you should check out the 4 | rewrite branch (in beta) which has lots of nice updates. 5 | 6 | pencil is a monitoring frontend for graphite. It runs a webserver that dishes 7 | out pretty graphite urls in hopefully interesting and intuitive layouts. 8 | 9 | Some features are: 10 | 11 | * Easy configuration 12 | 13 | Pretty much anything you'd want to do with the graphite composer can be coded 14 | into pencil configs using YAML syntax. 15 | 16 | * Implicit collection of host and cluster data 17 | 18 | pencil picks these up from graphite without you having to explicitly define 19 | them. You need only supply the metrics; see for 20 | configuration details. 21 | 22 | * Relative and absolute timespecs with auto-refresh 23 | 24 | Timeslices are measured in terms of a (possibly relative) starting time and a 25 | duration. Timespecs are parsed using the 26 | [chronic](http://chronic.rubyforge.org/) and 27 | [chronic_duration](https://github.com/hpoydar/chronic_duration) gems. 28 | 29 | You can use pencil in "tail-mode" (i.e. constant refresh, looking at last 30 | couple hours of data) or to view a particular timeslice in the past. 31 | 32 | * time-quantum support 33 | 34 | Requests can be mapped into discrete intervals (e.g.. 30 seconds) so that all 35 | requests within a window are treated as if they are the same request. This 36 | eases the burden on graphite to generate real-time images that are only 37 | slightly different than those generated by earlier requests. It also makes it 38 | easy to add some sort of caching layer. 39 | 40 | * permalinks 41 | 42 | Turn a relative timeslice (such as the last 8 hours) into an absolute one for 43 | placing in bug reports and all sorts of other useful things. 44 | 45 | * Lots of views and navigation UI for bouncing around them 46 | 47 | Global, cluster, dashboard, and host views! 48 | 49 | ## INSTALL 50 | 51 | gem install pencil 52 | 53 | Dependencies are: 54 | 55 | * rack 56 | * sinatra 57 | * mongrel 58 | * json 59 | * chronic 60 | * chronic_duration 61 | * (fixme versions) 62 | 63 | ## SETUP 64 | 65 | You should have a working graphite installation. Your metrics need to be 66 | composed of three pieces: 67 | 68 | * "%m", _METRIC_ (the common part of each graphite path) 69 | * "%c", _CLUSTER_ (cluster name, varies with query, must not contain periods) 70 | * "%h", _HOST_ (host name, varies with query, must not contain periods) 71 | 72 | The :metric_format string is specified in a configuration file (see below), and 73 | defaults to %m.%c.%h". It should contain only one %m, but is otherwise mostly 74 | unrestricted. 75 | 76 | You need to set up YAML configuration files in order for pencil to work. Pencil 77 | searches the current directory (or, with -d DIR, DIR) for YAML files to load. 78 | 79 | The important top-level configuration keys are: 80 | 81 | * :config: 82 | * :graphs: 83 | * :dashboards: 84 | 85 | See examples/ for an example configuration directory. Here's 86 | an example pencil.yml, which contains general configuration options: 87 | 88 | :config: 89 | :graphite_url URL # graphite URL 90 | :url_opts 91 | :width: 1000 92 | :height: 400 93 | :fontSize: 15 94 | :start: "8 hours ago" # in chronic timespec format 95 | :template: "noc" 96 | :yMin: 0 97 | :margin: 5 98 | :thickness: 2 99 | 100 | :refresh_rate: 60 # how often to refresh the view 101 | :host_sort: "numeric" # add this if you want to sort hosts numerically 102 | :quantum: 30 # map requests to 30 second intervals 103 | :date_format: "%X %x" # strftime 104 | :metric_format: "%m.%c.%h" #%m metric, %c cluster, %h host 105 | 106 | A graph is a name, title, collection of targets, and some other options. 107 | A target is a metric => options map; see docs/pencil_options.md for details on 108 | supported options. It looks like (in YAML): 109 | 110 | graph_name: # name pencil references this graph by 111 | title: "graph_title" # title displayed on graph 112 | targets: # list of graphite targets 113 | metric.1: 114 | :key: key # key displayed on the legend for this metric 115 | :color: color # color on the graph for this metric 116 | metric.2: 117 | :key: key 118 | :color: color 119 | [...] 120 | hosts: ["hosts1*", "test*_web", "hosts2*"] # filter on hosts 121 | areaMode: stacked 122 | 123 | (Note that in any case where you would use a hash you may use an 124 | [omap](http://www.yaml.org/spec/current.html#id2504191) instead, to ensure 125 | the order in which options are applied) 126 | 127 | Graph-level options are applied to the graph as a whole, where target-level 128 | options apply only to the specific target. 129 | 130 | Similarly, a dashboard is a name, title, collection of graphs, and some other 131 | options. It looks like (in YAML): 132 | 133 | dash_name: 134 | title: dash_title 135 | graphs: 136 | - graph1: 137 | hosts: ["sync*_web"] # hosts filter specific to this graph 138 | other_opt: val # possibly other options in the future 139 | - graph2: 140 | [...] 141 | hosts: ["filter1*", "*filter2"] 142 | 143 | A graph is just a graph name => options hash. Options specified in a dashboard 144 | for a graph override the options in the original graph's definition when 145 | displaying the dashboard. 146 | 147 | Pencil loads all the yaml files in its configuration directory and 148 | subdirectories. To facilitate organization of graphs and dashboards into 149 | multiple files the :graphs and :dashboards top-level keys are merged 150 | recursively during the load. The resulting pencil data structure will 151 | include all graphs and dashboards defined under these keys. 152 | 153 | A few words on host filters: two wildcards are supported: "*" and "#". 154 | "*" consumes as /.*/ (like a shell wildcard) and "#" as /\d+/ (one or more 155 | digits). Be aware that "#" may require explicit enumeration in graphite URLs 156 | (and is currently implemented in this way) and you might have to configure 157 | Apache to accept longer URLs if you have many hosts that match a "#". 158 | 159 | ### complex metrics and dashboard graph-level options 160 | A simple target is just a metric => options map. Targets can also be complex, 161 | where the key is an ordered list of simple targets. This is useful, for 162 | example, if you want to graph the summation of a list of metrics (see 163 | docs/pencil_options.md for a list of supported transforms). Complex targets are 164 | denoted with the YAML's 165 | [? : complex-key syntax](http://www.yaml.org/spec/current.html#id2503185), and 166 | look like this: 167 | 168 | memory_graph: 169 | title: "memory usage" 170 | targets: 171 | ? - system.memory.total: 172 | opt1: value 173 | opt2: 174 | - system.memory.free: 175 | - system.memory.cached: 176 | - system.memory.shared: 177 | - system.memory.buffers: 178 | : 179 | :key: "memory used" 180 | :color: green 181 | :diffSeries: 182 | 183 | As you can see, the memory used is computed by taking the difference of the 184 | total memory and its many uses. 185 | 186 | ## RUNNING THE SERVER 187 | Once you've set up the configs, you should be able to just run 188 | 189 | pencil 190 | 191 | and get something up on localhost:9292 192 | 193 | From there you should be able to click around and see various interesting 194 | collections of graphs. 195 | 196 | With no options, pencil looks in the current directory for YAML files and loads 197 | them. 198 | 199 | You can bind to a specific port with -p PORT and specify a configuration 200 | directory with -d DIR. Other rack-related options will be added at some point 201 | in the future. 202 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | -------------------------------------------------------------------------------- /bin/pencil: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pencil' 3 | 4 | Pencil::App.run! 5 | -------------------------------------------------------------------------------- /docs/pencil_options.md: -------------------------------------------------------------------------------- 1 | # Pencil Options 2 | Pencil configuration files are written in YAML. When pencil starts up, it 3 | searches for these files and loads them. See the main README.md for how 4 | these files should look. 5 | 6 | ## General Configuration 7 | 8 | These are options that go under the :config key in pencil configuration files. 9 | 10 | * :graphite_url [String, required, no default] 11 | 12 | The url of your graphite instance. 13 | 14 | * :url_opts [Hash, required, no default] 15 | 16 | A map of default graph options. 17 | 18 | In addition to graph-level options, an important default option 19 | you should set under :url_opts is 20 | 21 | :start: TIMESPEC 22 | 23 | TIMESPEC should be a 24 | [chronic](http://chronic.rubyforge.org/)-parsable time, and to be useful 25 | should be relative to the current time (e.g. "8 hours ago") 26 | 27 | * :refresh_rate [Fixnum, optional, default 60] 28 | 29 | How often to refresh a changing view, in seconds. 30 | 31 | This doesn't apply to timeslices that aren't varying (i.e not current, see 32 | :now_threshold). 33 | 34 | Set this to false to disable automatic refreshing. 35 | 36 | * :host_sort [["builtin", "numeric", "sensible"], optional, default "sensible"] 37 | 38 | Set to "builtin" to sort using ruby's builtin String sort. 39 | 40 | Set to "numeric" to sort hosts numerically (i.e. match secondarily on the 41 | first \d+). 42 | 43 | Set to "sensible" if you want to sort like this: 44 | 45 | http://www.bofh.org.uk/2007/12/16/comprehensible-sorting-in-ruby 46 | 47 | * :quantum [Fixnum, optional, no default value] 48 | 49 | Map requests to NUM second intervals. Pencil floors request times to the 50 | minute, and does some modular arithmetic to do this mapping. This is 51 | especially useful for implementing a caching layer, so that many requests 52 | coming in near-simultaneously won't require graphite to generate different 53 | images for each request. 54 | 55 | Adding &noq=1 to a pencil url will disable this in case you need 56 | super-granularity for some reason, but you didn't hear it from me. 57 | 58 | * :date_format [String, optional, default "%X %x"] 59 | 60 | strftime format for displaying dates. 61 | 62 | * :metric_format [String, optional, default "%m.%c.%h"] 63 | 64 | The format your graphite metrics are stored in. For pencil to work your 65 | metrics need to be composed of three distinct pieces, concatenated in some 66 | regular fashion. The format strings are 67 | 68 | * %m metric 69 | * %c cluster 70 | * %h host 71 | 72 | If you want a literal %[mch] in your metric format string you likely have 73 | bigger problems than not being able to do so. 74 | 75 | * :now\_threshold: [Fixnum, optional, default 300] 76 | 77 | How many seconds before Time.now an end time is considered to still be 'now', 78 | for the purposes of adding meta-refresh and displaying time intervals. 79 | 80 | * :use_color: [Boolean, optional, default false] 81 | 82 | In graphite 0.9.8 and earlier coloring of targets sucks. Coloring is not done 83 | on a per-target basis, but rather by an additional parameter 84 | "colorList". Pencil handles colors by appending a target's color parameter 85 | (or a suitable default) onto this list. Targets are processed in order and 86 | consume the first color available in colorList. 87 | 88 | This sucks because nonexistent targets occasionally produced by pencil don't 89 | consume their associated color, which screws up the coloring of every 90 | subsequent metric. 91 | 92 | The latest Graphite trunk supports color as a property of a target, set with 93 | color(target, "COLOR"). If you are using a new graphite and want to take 94 | advantage of this more accurate coloring technique set :use_color to true, 95 | and bask in the glory of color correctness. 96 | 97 | ## Graph-level Options 98 | This is a list of the supported graph-level options for pencil, which 99 | correspond to request(image)-level options for graphite. These options are 100 | key-value pairs, and are passed directly to graphite. Here is the list, with 101 | minor annotations: 102 | 103 | * vtitle: String (y-axis label) 104 | * yMin: Fixnum 105 | * yMax: Fixnum 106 | * lineWidth: Fixnum (line thickness in pixels) 107 | * areaMode: \[first, all, stacked\] (see graphite documentation) 108 | * template: \[noc, alphas\] (alphas inverts colors) 109 | * lineMode: staircase 110 | * bgcolor: String 111 | * graphOnly: bool (hide legend, axes, grid) 112 | * hideAxes: bool 113 | * hideGrid: bool 114 | * hideLegend: bool 115 | * fgcolor: String 116 | * fontSize: Fixnum 117 | * fontName: String (see your graphite instance for available fonts) 118 | * fontItalic: bool 119 | * fontBold: bool 120 | 121 | ## Target-level Options 122 | This is a list of the supported target-level options for pencil. These are 123 | mostly a list of transformations graphite supports, including summation and 124 | scaling of metrics. You can apply them to individual metrics, or lists of 125 | metrics. See the example configs for how this works. Also see the graphite 126 | composer for the effects of these options, many of which are untested. 127 | 128 | A note on the divideSeries case: for aggregate graphs, pencil takes the ratio 129 | of the sums, as opposed to the sums of the ratios, for ease of implementation 130 | and because it makes some sense. If you're not a fan of 80k URLs then you will 131 | agree. divideSeries should only ever be used with two targets, like this: 132 | 133 | ? - stats.timers.mysql.innodb.threads_active_read.mean: 134 | - stats.timers.mysql.innodb.threads_total_read.mean: 135 | : 136 | !omap 137 | - :divideSeries: 138 | 139 | (Notice the omap, as opposed to a hash, to impose an order in which options are 140 | applied) 141 | 142 | ### Combinations 143 | These functions take an arbitrary number of targets (usually simple metrics) 144 | for arguments. 145 | 146 | * sumSeries 147 | * averageSeries 148 | * minSeries 149 | * maxSeries 150 | * group 151 | 152 | ### Transformations 153 | Some of these options take a single argument. 154 | 155 | * scale 156 | * offset 157 | * derivative 158 | * integral 159 | * nonNegativeDerivative 160 | * log BASE 161 | * timeShift 162 | * summarize 163 | * hitcount 164 | 165 | ### Calculations 166 | These functions take an arbitrary number of targets (usually simple metrics) 167 | for arguments. 168 | 169 | * movingAverage 170 | * stdev 171 | * asPercent 172 | * diffSeries 173 | * divideSeries (NOTE: takes exactly 1 series as divisor) 174 | 175 | ### Filters 176 | Most of these options take a single argument. 177 | 178 | * highestCurrent 179 | * lowestCurrent 180 | * nPercentile 181 | * currentAbove 182 | * currentBelow 183 | * highestAverage 184 | * lowestAverage 185 | * averageAbove 186 | * averageBelow 187 | * maximumAbove 188 | * maximumBelow 189 | * sortByMaxima 190 | * minimalist 191 | * limit 192 | * exclude 193 | 194 | ### Special Operations 195 | * alias 196 | * key (alias for alias) 197 | * cumulative 198 | * drawAsInfinite 199 | * lineWidth 200 | * dashed 201 | * keepLastValue 202 | * substr 203 | * threshold 204 | * color 205 | 206 | Note: key and color are applied after all other options are applied. 207 | 208 | key is interpreted differently from the other options, which are more 209 | simply translated. 210 | 211 | color's interpretation depends on whether :use_color is set. 212 | -------------------------------------------------------------------------------- /examples/dashboards.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :dashboards: 3 | syncstorage: 4 | title: syncstorage_overview 5 | graphs: 6 | - syncstorage_qps: 7 | - syncstorage_401: 8 | - syncstorage_exceptions: 9 | - cpu_usage: 10 | - memory_usage: 11 | - network_traffic: 12 | hosts: ["sync*_web", "wp-web*"] 13 | syncreg: 14 | title: syncreg_overview 15 | graphs: 16 | - syncreg_qps: 17 | - syncreg_exceptions: 18 | - cpu_usage: 19 | - memory_usage: 20 | - network_traffic: 21 | hosts: ["wp-reg*"] 22 | syncsreg: 23 | title: syncsreg_overview 24 | graphs: 25 | - syncsreg_qps: 26 | - syncsreg_exceptions: 27 | - cpu_usage: 28 | - memory_usage: 29 | - network_traffic: 30 | hosts: ["wp-sreg*"] 31 | ldap_slaves: 32 | title: LDAP slaves 33 | graphs: 34 | - ldap_ops: 35 | - load_average: 36 | - cpu_usage: 37 | - network_traffic: 38 | hosts: ["slave*_ldap", "wp-slave*"] 39 | ldap_masters: 40 | title: LDAP masters 41 | graphs: 42 | - ldap_ops: 43 | - load_average: 44 | - cpu_usage: 45 | - network_traffic: 46 | hosts: ["master*_ldap", "wp-master*"] 47 | sync_mysql: 48 | title: sync mysql 49 | graphs: 50 | - mysql_ops: 51 | - load_average: 52 | - cpu_usage: 53 | - network_traffic: 54 | - mysql_pending_file_io: 55 | - mysql_history_length: 56 | - mysql_file_io_threads: 57 | - mysql_file_io_threads_usage: 58 | hosts: ["sync*_db", "wp-db*"] 59 | sync_node_alloc: 60 | title: sync node allocation 61 | graphs: 62 | - sync_node_alloc: 63 | - sync_node_actives: 64 | hosts: ["wp-adm01"] 65 | -------------------------------------------------------------------------------- /examples/graphs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :graphs: 3 | syncstorage_qps: 4 | title: "syncstorage qps" 5 | targets: 6 | syncstorage.request_rate.200: 7 | :key: 200 8 | :color: green 9 | syncstorage.request_rate.302: 10 | :key: 302 11 | :color: blue 12 | syncstorage.request_rate.401: 13 | :key: 401 14 | :color: brown 15 | syncstorage.request_rate.404: 16 | :key: 404 17 | :color: purple 18 | syncstorage.request_rate.503: 19 | :key: 503 20 | :color: red 21 | areaMode: stacked 22 | hosts: ["sync*_web", "test*_web", "wp-web*"] 23 | 24 | syncstorage_401: 25 | title: "syncstorage 401s" 26 | targets: 27 | syncstorage.request_rate.401: 28 | :key: 200 29 | :color: brown 30 | hosts: ["sync*_web", "test*_web", "wp-web*"] 31 | 32 | syncstorage_exceptions: 33 | title: "syncstorage exceptions" 34 | targets: 35 | syncstorage.log.exception: 36 | :key: exceptions/s 37 | :color: red 38 | hosts: ["sync*_web", "test*_web", "wp-web*"] 39 | 40 | syncreg_qps: 41 | title: "syncreg qps" 42 | targets: 43 | syncreg.request_rate.200: 44 | :key: 200 45 | :color: green 46 | syncreg.request_rate.302: 47 | :key: 302 48 | :color: blue 49 | syncreg.request_rate.401: 50 | :key: 401 51 | :color: brown 52 | syncreg.request_rate.404: 53 | :key: 404 54 | :color: purple 55 | syncreg.request_rate.503: 56 | :key: 503 57 | :color: red 58 | areaMode: stacked 59 | hosts: ["wp-reg*"] 60 | 61 | syncreg_exceptions: 62 | title: "syncreg exceptions" 63 | targets: 64 | syncreg.log.exception: 65 | :key: exceptions/s 66 | :color: red 67 | hosts: ["wp-reg*"] 68 | 69 | syncsreg_qps: 70 | title: "syncsreg qps" 71 | targets: 72 | syncsreg.request_rate.200: 73 | :key: 200 74 | :color: green 75 | syncsreg.request_rate.302: 76 | :key: 302 77 | :color: blue 78 | syncsreg.request_rate.401: 79 | :key: 401 80 | :color: brown 81 | syncsreg.request_rate.404: 82 | :key: 404 83 | :color: purple 84 | syncsreg.request_rate.503: 85 | :key: 503 86 | :color: red 87 | areaMode: stacked 88 | hosts: ["wp-sreg*"] 89 | 90 | syncsreg_exceptions: 91 | title: "syncsreg exceptions" 92 | targets: 93 | syncsreg.log.exception: 94 | :key: exceptions/s 95 | :color: red 96 | hosts: ["wp-sreg*"] 97 | 98 | cpu_usage: 99 | title: "cpu usage" 100 | targets: 101 | system.cpu.system: 102 | :key: CPU/system 103 | :color: yellow 104 | system.cpu.wio: 105 | :key: CPU/wio 106 | :color: red 107 | system.cpu.nice: 108 | :key: CPU/nice 109 | :color: green 110 | system.cpu.user: 111 | :key: CPU/user 112 | :color: blue 113 | areaMode: stacked 114 | hosts: ["*"] 115 | 116 | load_average: 117 | title: "1 minute load average" 118 | targets: 119 | system.load.1: 120 | :key: load 121 | hosts: ["*"] 122 | 123 | network_traffic: 124 | title: "network traffic" 125 | targets: 126 | system.network.in: 127 | :key: bits in 128 | :color: green 129 | system.network.out: 130 | :key: bits out 131 | :color: blue 132 | 133 | hosts: ["*"] 134 | scale: 8 # bytes -> bits 135 | 136 | ldap_ops: 137 | title: "ldap operations" 138 | targets: 139 | slapd.ops.add: 140 | :key: add 141 | :color: red 142 | slapd.ops.bind: 143 | :key: bind 144 | :color: brown 145 | slapd.ops.modify: 146 | :key: modify 147 | :color: purple 148 | slapd.ops.search: 149 | :key: search 150 | :color: green 151 | 152 | areaMode: stacked 153 | hosts: ["slave*ldap", "master*ldap", "wp-master*"] 154 | 155 | mysql_ops: 156 | title: "mysql operations" 157 | targets: 158 | mysql.ops.delete: 159 | :key: delete 160 | :color: red 161 | mysql.ops.insert: 162 | :key: insert 163 | :color: brown 164 | mysql.ops.update: 165 | :key: update 166 | :color: purple 167 | mysql.ops.select: 168 | :key: select 169 | :color: green 170 | 171 | areaMode: stacked 172 | hosts: ["sync*_db", "wp-db*"] 173 | 174 | memory_usage: 175 | title: "system memory usage" 176 | targets: 177 | ? - system.memory.total: 178 | - system.memory.free: 179 | - system.memory.cached: 180 | - system.memory.shared: 181 | - system.memory.buffers: 182 | : 183 | :key: used 184 | :color: green 185 | :diffSeries: 186 | system.memory.total: 187 | :key: total 188 | :color: white 189 | system.memory.cached: 190 | :key: cached 191 | :color: purple 192 | 193 | scale: 1024 # M -> G 194 | hosts: ["*"] 195 | # some random options for demonstration 196 | # template: alphas 197 | # bgcolor: "#AFFFFF" 198 | # vtitle: tvtnolgrf 199 | # lineWidth: 20 200 | 201 | sync_node_alloc: 202 | title: "node alloc rate" 203 | targets: 204 | sync.users.alloc_rate.scl2: 205 | :key: scl2 alloc/s 206 | :color: green 207 | sync.users.alloc_rate.phx1: 208 | :key: phx1 alloc/s 209 | :color: blue 210 | 211 | hosts: ["wp-adm01"] 212 | 213 | sync_node_actives: 214 | title: "node actives" 215 | targets: 216 | sync.users.active.scl2: 217 | :key: scl2 actives 218 | :color: green 219 | sync.users.active.phx1: 220 | :key: phx1 actives 221 | :color: blue 222 | 223 | hosts: ["wp-adm01"] 224 | 225 | mysql_pending_file_io: 226 | title: "mysql pending file i/o" 227 | targets: 228 | stats.timers.mysql.innodb.pending_read.mean: 229 | :key: read 230 | :color: green 231 | stats.timers.mysql.innodb.pending_write.mean: 232 | :key: write 233 | :color: blue 234 | stats.timers.mysql.innodb.pending_ibuf.mean: 235 | :key: ibuf 236 | :color: brown 237 | stats.timers.mysql.innodb.pending_log.mean: 238 | :key: log 239 | :color: red 240 | stats.timers.mysql.innodb.pending_sync.mean: 241 | :key: sync 242 | :color: yellow 243 | 244 | hosts: ["wp-db*", "sync*_db"] 245 | 246 | mysql_history_length: 247 | title: "mysql transaction history" 248 | targets: 249 | stats.timers.mysql.innodb.history_length.mean: 250 | :key: read 251 | :color: green 252 | 253 | hosts: ["wp-db*", "sync*_db"] 254 | 255 | mysql_file_io_threads_usage: 256 | title: "mysql file i/o threads active (% used)" 257 | targets: 258 | ? - stats.timers.mysql.innodb.threads_active_read.mean: 259 | - stats.timers.mysql.innodb.threads_total_read.mean: 260 | : 261 | !omap 262 | - :divideSeries: 263 | - :color: green 264 | - :asPercent: 1.0 265 | - :movingAverage: 10 266 | - :key: read 267 | ? - stats.timers.mysql.innodb.threads_active_write.mean: 268 | - stats.timers.mysql.innodb.threads_total_write.mean: 269 | : 270 | !omap 271 | - :divideSeries: 272 | - :color: blue 273 | - :asPercent: 1.0 274 | - :movingAverage: 10 275 | - :key: write 276 | ? - stats.timers.mysql.innodb.threads_active_ibuf.mean: 277 | - stats.timers.mysql.innodb.threads_total_ibuf.mean: 278 | : 279 | !omap 280 | - :divideSeries: 281 | - :color: brown 282 | - :asPercent: 1.0 283 | - :movingAverage: 10 284 | - :key: ibuf 285 | ? - stats.timers.mysql.innodb.threads_active_log.mean: 286 | - stats.timers.mysql.innodb.threads_total_log.mean: 287 | : 288 | !omap 289 | - :divideSeries: 290 | - :color: red 291 | - :asPercent: 1.0 292 | - :movingAverage: 10 293 | - :key: log 294 | hosts: ["wp-db*", "sync*_db"] 295 | 296 | mysql_file_io_threads: 297 | title: "mysql file i/o threads" 298 | targets: 299 | stats.timers.mysql.innodb.threads_active_read.mean: 300 | !omap 301 | - :color: green 302 | - :movingAverage: 10 303 | - :key: read 304 | stats.timers.mysql.innodb.threads_active_write.mean: 305 | !omap 306 | - :color: blue 307 | - :movingAverage: 10 308 | - :key: write 309 | stats.timers.mysql.innodb.threads_active_ibuf.mean: 310 | !omap 311 | - :color: brown 312 | - :movingAverage: 10 313 | - :key: ibuf 314 | stats.timers.mysql.innodb.threads_active_log.mean: 315 | !omap 316 | - :color: red 317 | - :movingAverage: 10 318 | - :key: log 319 | 320 | hosts: ["wp-db*", "sync*_db"] 321 | -------------------------------------------------------------------------------- /examples/pencil.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :config: 3 | :graphite_url: http://graphite-global.phx.weave.mozilla.com 4 | :default_colors: 5 | ["blue", "green", "yellow", "red", "purple", "brown", "aqua", "gold"] 6 | :url_opts: 7 | :width: 1000 8 | :height: 400 9 | :fontSize: 15 10 | :start: "8 hours ago" # chronic timespec 11 | :template: "noc" 12 | :yMin: 0 13 | :margin: 5 14 | :thickness: 2 15 | 16 | :refresh_rate: 60 17 | :host_sort: "sensible" 18 | :quantum: 30 # map requests to 30 second intervals 19 | :date_format: "%X %x" # strftime 20 | :metric_format: "%m.%c.%h" #%m metric, %c cluster, %h host 21 | :now_threshold: 300 # Time.at(Time.now - now_threshold) considered Time.now 22 | 23 | # set this if you have a new graphite that supports color(series, "color") 24 | # :use_color: true 25 | -------------------------------------------------------------------------------- /lib/pencil.rb: -------------------------------------------------------------------------------- 1 | require "erb" 2 | require "rack" 3 | require "sinatra/base" 4 | require "json" 5 | require "open-uri" 6 | require "yaml" 7 | require "chronic" 8 | require "chronic_duration" 9 | require "optparse" 10 | 11 | require "pencil/version" 12 | require "pencil/config" 13 | require "pencil/helpers" 14 | require "pencil/models" 15 | require "pencil/rubyfixes" 16 | 17 | module Pencil 18 | class App < Sinatra::Base 19 | include Pencil::Models 20 | helpers Pencil::Helpers 21 | config = Pencil::Config.new 22 | set :config, config 23 | set :port, config.global_config[:port] 24 | set :run, true 25 | use Rack::Session::Cookie, :expire_after => 126227700 # 4 years 26 | set :root, File.expand_path('../pencil', __FILE__) 27 | set :static, true 28 | set :logging, true 29 | set :erb, :trim => '-' 30 | 31 | def initialize(settings={}) 32 | super 33 | end 34 | 35 | before do 36 | session[:not] #fixme kludge is back 37 | @request_time = Time.now 38 | @dashboards = Dashboard.all 39 | @no_graphs = false 40 | # time stuff 41 | start = param_lookup("start") 42 | duration = param_lookup("duration") 43 | 44 | @stime = Chronic.parse(start) 45 | if @stime 46 | @stime -= @stime.sec unless @params["noq"] 47 | elsif start =~ /^\d+$/ #epoch 48 | @stime = Time.at start.to_i 49 | end 50 | 51 | if duration 52 | @duration = ChronicDuration.parse(duration) || 0 53 | else 54 | @duration = @request_time.to_i - @stime.to_i 55 | end 56 | 57 | unless @params["noq"] 58 | @duration -= (@duration % settings.config.global_config[:quantum]||1) 59 | end 60 | 61 | if @stime 62 | @etime = Time.at(@stime + @duration) 63 | @etime = @request_time if @etime > @request_time 64 | else 65 | @etime = @request_time 66 | end 67 | 68 | session[:stime] = @stime.to_i.to_s 69 | session[:etime] = @etime.to_i.to_s 70 | # fixme reload hosts after some expiry 71 | end 72 | 73 | get %r[^/(dash/?)?$] do 74 | @no_graphs = true 75 | if settings.config.clusters.size == 1 76 | redirect append_query_string("/dash/#{settings.config.clusters.first}") 77 | else 78 | redirect append_query_string('/dash/global') 79 | end 80 | end 81 | 82 | get '/dash/:cluster/:dashboard/:zoom/?' do 83 | @cluster = params[:cluster] 84 | @dash = Dashboard.find(params[:dashboard]) 85 | raise "Unknown dashboard: #{params[:dashboard].inspect}" unless @dash 86 | 87 | @zoom = nil 88 | @dash.graphs.each do |graph| 89 | @zoom = graph if graph.name == params[:zoom] 90 | end 91 | raise "Unknown zoom parameter: #{params[:zoom]}" unless @zoom 92 | 93 | @title = "dashboard :: #{@cluster} :: #{@dash['title']} :: #{params[:zoom]}" 94 | 95 | if @cluster == "global" 96 | erb :'dash-global-zoom' 97 | else 98 | erb :'dash-cluster-zoom' 99 | end 100 | end 101 | 102 | get '/dash/:cluster/:dashboard/?' do 103 | @cluster = params[:cluster] 104 | @dash = Dashboard.find(params[:dashboard]) 105 | raise "Unknown dashboard: #{params[:dashboard].inspect}" unless @dash 106 | 107 | @title = "dashboard :: #{@cluster} :: #{@dash['title']}" 108 | 109 | if @cluster == "global" 110 | erb :'dash-global' 111 | else 112 | erb :'dash-cluster' 113 | end 114 | end 115 | 116 | get '/dash/:cluster/?' do 117 | @no_graphs = true 118 | @cluster = params[:cluster] 119 | if @cluster == "global" 120 | @title = "Overview" 121 | erb :global 122 | else 123 | @title = "cluster :: #{params[:cluster]}" 124 | erb :cluster 125 | end 126 | end 127 | 128 | get '/host/:cluster/:host/?' do 129 | @host = Host.find_by_name_and_cluster(params[:host], params[:cluster]) 130 | @cluster = @host.cluster 131 | raise "Unknown host: #{params[:host]} in #{params[:cluster]}" unless @host 132 | 133 | @title = "#{@host.cluster} :: host :: #{@host.name}" 134 | 135 | erb :host 136 | end 137 | 138 | get '/process' do 139 | case params["action"] 140 | when "Save" 141 | # fixme make sure not to save shitty values for :start 142 | puts 'saving prefs' 143 | params.each do |k ,v| 144 | next if [:etime, :stime, :duration].member?(k.to_sym) 145 | session[k] = v unless v.empty? 146 | end 147 | redirect URI.parse(request.referrer).path 148 | when "Clear" 149 | puts URI.parse(request.referrer).path 150 | redirect URI.parse(request.referrer).path 151 | when "Reset" 152 | puts "clearing prefs" 153 | session.clear 154 | redirect URI.parse(request.referrer).path 155 | when "Submit" 156 | # fixme offensive to sensibility 157 | redirect URI.parse(request.referrer).path + "?" + \ 158 | request.query_string.sub("&action=Submit", "").sub("?action=Submit", "") 159 | end 160 | end 161 | end # Pencil::App 162 | end # Pencil 163 | -------------------------------------------------------------------------------- /lib/pencil/config.rb: -------------------------------------------------------------------------------- 1 | require 'map' 2 | require "pencil/models" 3 | 4 | module Pencil 5 | class Config 6 | include Pencil::Models 7 | 8 | attr_reader :dashboards 9 | attr_reader :graphs 10 | attr_reader :hosts 11 | attr_reader :clusters 12 | attr_reader :global_config 13 | 14 | def initialize 15 | port = 9292 16 | @rawconfig = {} 17 | @confdir = "." 18 | @recursive = false 19 | 20 | optparse = OptionParser.new do |o| 21 | o.on("-d", "--config-dir DIR", 22 | "location of the config directory (default .)") do |arg| 23 | @confdir = arg 24 | @recursive = true 25 | end 26 | o.on("-p", "--port PORT", "port to bind to (default 9292)") do |arg| 27 | port = arg.to_i 28 | end 29 | end 30 | 31 | optparse.parse! 32 | reload! 33 | @global_config[:port] = port 34 | end 35 | 36 | def reload! 37 | # only do a recursive search if "-d" is specified 38 | configs = Dir.glob("#{@confdir}/#{@recursive ? '**/' : ''}*.y{a,}ml") 39 | 40 | configs.each do |c| 41 | yml = YAML.load(File.read(c)) 42 | next unless yml 43 | @rawconfig[:config] = yml[:config] if yml[:config] 44 | a = @rawconfig[:dashboards] 45 | b = yml[:dashboards] 46 | c = @rawconfig[:graphs] 47 | d = yml[:graphs] 48 | 49 | if a && b 50 | a.merge!(b) 51 | elsif b 52 | @rawconfig[:dashboards] = b 53 | end 54 | if c && d 55 | c.merge!(d) 56 | elsif d 57 | @rawconfig[:graphs] = d 58 | end 59 | end 60 | @rawconfig = Map(@rawconfig) 61 | 62 | [:graphs, :dashboards, :config].each do |c| 63 | if not @rawconfig[c.to_s] 64 | raise "Missing config name '#{c.to_s}'" 65 | end 66 | end 67 | 68 | @global_config = @rawconfig[:config] 69 | # do some sanity checking of other configuration parameters 70 | [:graphite_url, :url_opts].each do |c| 71 | if not @global_config[c] 72 | raise "Missing config name '#{c.to_s}'" 73 | end 74 | end 75 | 76 | # possibly check more url_opts here as well 77 | if @global_config[:url_opts][:start] 78 | if !ChronicDuration.parse(@global_config[:url_opts][:start]) 79 | raise "bad default timespec in :url_opts" 80 | end 81 | end 82 | 83 | @global_config[:default_colors] ||= 84 | ["blue", "green", "yellow", "red", "purple", "brown", "aqua", "gold"] 85 | 86 | if @global_config[:refresh_rate] 87 | duration = ChronicDuration.parse(@global_config[:refresh_rate].to_s) 88 | if !duration 89 | raise "couldn't parse key :refresh_rate" 90 | end 91 | @global_config[:refresh_rate] = duration 92 | end 93 | 94 | @global_config[:metric_format] ||= "%m.%c.%h" 95 | if @global_config[:metric_format] !~ /%m/ 96 | raise "missing metric (%m) in :metric_format" 97 | elsif @global_config[:metric_format] !~ /%c/ 98 | raise "missing cluster (%c) in :metric_format" 99 | elsif @global_config[:metric_format] !~ /%h/ 100 | raise "missing host (%h) in :metric_format" 101 | end 102 | 103 | graphs_new = [] 104 | @rawconfig[:graphs].each do |name, config| 105 | graphs_new << Graph.new(name, config.merge(@global_config)) 106 | end 107 | 108 | dashboards_new = [] 109 | @rawconfig[:dashboards].each do |name, config| 110 | dashboards_new << Dashboard.new(name, config.merge(@global_config)) 111 | end 112 | 113 | hosts_new = Set.new 114 | clusters_new = Set.new 115 | 116 | # generate host and cluster information at init time 117 | graphs_new.each do |g| 118 | hosts, clusters = g.hosts_clusters 119 | hosts.each { |h| hosts_new << h } 120 | clusters.each { |h| clusters_new << h } 121 | end 122 | 123 | @dashboards, @graphs = dashboards_new, graphs_new 124 | @hosts, @clusters = hosts_new, clusters_new 125 | end 126 | end # Pencil::Config 127 | end # Pencil 128 | -------------------------------------------------------------------------------- /lib/pencil/config.ru: -------------------------------------------------------------------------------- 1 | require 'pencil' 2 | run Pencil::App 3 | -------------------------------------------------------------------------------- /lib/pencil/helpers.rb: -------------------------------------------------------------------------------- 1 | module Pencil::Helpers 2 | include Pencil::Models 3 | 4 | @@prefs = [["Start Time", "start"], 5 | ["Duration", "duration"], 6 | ["Width", "width"], 7 | ["Height", "height"]] 8 | 9 | # convert keys to symbols before lookup 10 | def param_lookup(name) 11 | sym_hash = {} 12 | session.each { |k,v| sym_hash[k.to_sym] = v unless v.empty? } 13 | params.each { |k,v| sym_hash[k.to_sym] = v unless v.empty? } 14 | settings.config.global_config[:url_opts].merge(sym_hash)[name.to_sym] 15 | end 16 | 17 | def cluster_graph(g, cluster, title="wtf") 18 | image_url = \ 19 | @dash.render_cluster_graph(g, cluster, 20 | :title => title, 21 | :dynamic_url_opts => merge_opts) 22 | zoom_url = cluster_graph_link(@dash, g, cluster) 23 | return image_url, zoom_url 24 | end 25 | 26 | def cluster_graph_link(dash, g, cluster) 27 | link = dash.graph_opts[g]["click"] || 28 | "/dash/#{cluster}/#{dash.name}/#{g.name}" 29 | return append_query_string(link) 30 | end 31 | 32 | def cluster_zoom_graph(g, cluster, host, title) 33 | image_url = g.render_url([host.name], [cluster], :title => title, 34 | :dynamic_url_opts => merge_opts) 35 | zoom_url = cluster_zoom_link(cluster, host) 36 | return image_url, zoom_url 37 | end 38 | 39 | def cluster_zoom_link(cluster, host) 40 | return append_query_string("/host/#{cluster}/#{host}") 41 | end 42 | 43 | def suggest_cluster_links(clusters, g) 44 | links = [] 45 | clusters.each do |c| 46 | href = append_query_string("/dash/#{c}/#{params[:dashboard]}/#{g.name}") 47 | links << "#{c}" 48 | end 49 | return "zoom (" + links.join(", ") + ")" 50 | end 51 | 52 | def suggest_dashboards_links(host, graph) 53 | suggested = suggest_dashboards(host, graph) 54 | return "" if suggested.length == 0 55 | 56 | links = [] 57 | suggested.each do |d| 58 | links << "" + 59 | "#{d}" 60 | end 61 | return "(" + links.join(", ") + ")" 62 | end 63 | 64 | # it's mildly annoying that when this set is empty there're no uplinks 65 | # consider adding a link up to the cluster (which is best we can do) 66 | def suggest_dashboards(host, graph) 67 | ret = Set.new 68 | 69 | host.graphs.each do |g| 70 | Dashboard.find_by_graph(g).each do |d| 71 | valid, _ = d.get_valid_hosts(g, host['cluster']) 72 | ret << d.name if valid.member?(host) 73 | end 74 | end 75 | 76 | return ret 77 | end 78 | 79 | # generate the input box fields, filled in to current parameters if specified 80 | def input_boxes 81 | @prefs = @@prefs 82 | erb :'partials/input_boxes', :layout => false 83 | end 84 | 85 | def dash_link(dash, cluster) 86 | return append_query_string("/dash/#{cluster}/#{dash.name}") 87 | end 88 | 89 | def cluster_link(cluster) 90 | return append_query_string("/dash/#{cluster}") 91 | end 92 | 93 | def css_url 94 | style = File.join(settings.root, "public/style.css") 95 | mtime = File.mtime(style).to_i.to_s 96 | return \ 97 | %Q[] 98 | end 99 | 100 | def refresh 101 | if settings.config.global_config[:refresh_rate] != false && nowish 102 | rate = settings.config.global_config[:refresh_rate] || 60 103 | return %Q[] 104 | end 105 | end 106 | 107 | def hosts_selector(hosts, print_clusters=false) 108 | @print_clusters = print_clusters 109 | @hosts = hosts 110 | erb :'partials/hosts_selector', :layout => false 111 | end 112 | 113 | def append_query_string(str) 114 | v = str.dup 115 | # CGI.escapeHTML()? 116 | unless request.query_string.empty? 117 | v << "?#{request.query_string.gsub("&", "&")}" 118 | end 119 | return v 120 | end 121 | 122 | def merge_opts 123 | static_opts = ["cluster", "dashboard", "zoom", "host", "session_id"] 124 | opts = params.dup 125 | session.merge(opts).delete_if { |k,v| static_opts.member?(k) || v.empty? } 126 | end 127 | 128 | def shortcuts(str) 129 | @str = str 130 | erb :'partials/shortcuts', :layout => false 131 | end 132 | def cluster_switcher(clusters) 133 | @clusters = clusters 134 | erb :'partials/cluster_switcher', :layout => false 135 | end 136 | 137 | def dash_switcher 138 | erb :'partials/dash_switcher', :layout => false 139 | end 140 | 141 | def graph_switcher 142 | erb :'partials/graph_switcher', :layout => false 143 | end 144 | 145 | def cluster_selector 146 | @clusters = settings.config.clusters.sort + ["global"] 147 | erb :'partials/cluster_selector', :layout => false 148 | end 149 | 150 | def host_uplink 151 | link = "/dash/#{append_query_string(@host.cluster)}" 152 | "zoom out: #{@host.cluster}" 153 | end 154 | 155 | def graph_uplink 156 | link = append_query_string(request.path.split("/")[0..-2].join("/")) 157 | "zoom out: #{@dash}" 158 | end 159 | 160 | def dash_uplink 161 | link = append_query_string(request.path.split("/")[0..-2].join("/")) 162 | "zoom out: #{@params[:cluster]}" 163 | end 164 | 165 | # fixme this is a hack 166 | def header(str) 167 | <<-FOO 168 |
169 |

#{@title}

170 | 171 | #{str} 172 | 173 |
174 |
175 |

#{range_string}

176 | #{permalink unless @no_graphs} 177 |
178 | FOO 179 | end 180 | 181 | def nowish 182 | if settings.config.global_config[:now_threshold] == false 183 | return false 184 | end 185 | threshold = settings.config.global_config[:now_threshold] || 300 186 | return @request_time.to_i - @etime.to_i < threshold 187 | end 188 | 189 | def range_string 190 | format = settings.config.global_config[:date_format] || "%X %x" 191 | if @stime && @etime 192 | if nowish 193 | "timeslice: from #{@stime.strftime(format)}" 194 | else 195 | "timeslice: #{@stime.strftime(format)} - #{@etime.strftime(format)}" 196 | end 197 | else 198 | "invalid time range" 199 | end 200 | 201 | end 202 | 203 | def valid_time (s) 204 | Chronic.parse(s) || s =~ /^\d+$/ 205 | end 206 | 207 | def permalink 208 | return "" unless @stime && @duration 209 | format = "%F %T" # chronic REALLY understands this 210 | url = request.path + "?" 211 | url << "&start=#{@stime.strftime(format)}" 212 | url << "&duration=#{ChronicDuration.output(@duration)}" 213 | "permalink" 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /lib/pencil/models.rb: -------------------------------------------------------------------------------- 1 | require "pencil/models/dashboard" 2 | require "pencil/models/graph" 3 | require "pencil/models/host" 4 | -------------------------------------------------------------------------------- /lib/pencil/models/base.rb: -------------------------------------------------------------------------------- 1 | module Pencil 2 | module Models 3 | class Base 4 | @@objects = Hash.new { |h, k| h[k] = Hash.new } 5 | attr_reader :name 6 | 7 | def initialize(name, params={}) 8 | @name = name 9 | @match_name = name 10 | @params = params 11 | @@objects[self.class.to_s][name] = self 12 | end 13 | 14 | def self.find(name) 15 | return @@objects[self.name][name] rescue [] 16 | end 17 | 18 | def self.each(&block) 19 | h = @@objects[self.name] rescue {} 20 | h.each { |k, v| yield(k, v) } 21 | end 22 | 23 | def self.all 24 | return @@objects[self.name].values 25 | end 26 | 27 | def [](key) 28 | return @params[key] rescue [] 29 | end 30 | 31 | def match(glob) 32 | return true if glob == '*' 33 | # convert glob to a regular expression 34 | glob_re = /^#{Regexp.escape(glob).gsub('\*', '.*').gsub('\#', '\d+')}$/ 35 | return @match_name.match(glob_re) 36 | end 37 | 38 | def multi_match(globs) 39 | ret = false 40 | 41 | globs.each do |glob| 42 | ret = match(glob) 43 | break if ret 44 | end 45 | 46 | return ret 47 | end 48 | 49 | def to_s 50 | return @name 51 | end 52 | 53 | def <=>(other) 54 | return to_s <=> other.to_s 55 | end 56 | 57 | def update_params(hash) 58 | @params.merge!(hash) 59 | end 60 | 61 | # compose a metric using a :metric_format 62 | # format string with %c for metric, %c for cluster, and %h for host 63 | def compose_metric (m, c, h) 64 | @params[:metric_format].dup.gsub("%m", m).gsub("%c", c).gsub("%h", h) 65 | end 66 | 67 | # used to make sure partial metrics are not considered 68 | # e.g. foo.bar.baz.colo1.host1 but not 69 | # foo.bar.baz.subkey.subkey2[.colo1.host1] 70 | # where the latter is returned by the graphite expand api but not as a full 71 | # metric 72 | # essentially equivalent to matching like /[:METRIC:]$/ 73 | def compose_metric2 (m, c, h) 74 | compose_metric(m, c, h) + ".*" 75 | end 76 | end # Pencil::Models::Base 77 | end 78 | end # Pencil::Models 79 | -------------------------------------------------------------------------------- /lib/pencil/models/dashboard.rb: -------------------------------------------------------------------------------- 1 | require "pencil/models/base" 2 | require "pencil/models/graph" 3 | require "pencil/models/host" 4 | require "set" 5 | 6 | module Pencil 7 | module Models 8 | class Dashboard < Base 9 | attr_accessor :graphs 10 | attr_accessor :graph_opts 11 | 12 | def initialize(name, params={}) 13 | super 14 | 15 | @graphs = [] 16 | @graph_opts = {} 17 | params["graphs"].each do |n| 18 | if n.respond_to?(:keys) 19 | key = n.keys.first # should only be one key 20 | val = n.values.first 21 | g = Graph.find(key) 22 | @graph_opts[g] = val||{} 23 | else 24 | raise "Bad format for graph (must be a hash-y; #{n.class}:#{n.inspect} is not)" 25 | end 26 | 27 | @graphs << g if g 28 | end 29 | 30 | @valid_hosts_table = {} # cache calls to get_valid_hosts 31 | end 32 | 33 | def clusters 34 | clusters = Set.new 35 | @graphs.each { |g| clusters += get_valid_hosts(g)[1] } 36 | clusters.sort 37 | end 38 | 39 | def get_all_hosts(cluster=nil) 40 | hosts = Set.new 41 | clusters = Set.new 42 | @graphs.each do |g| 43 | h, c = get_valid_hosts(g, cluster) 44 | hosts += h 45 | clusters += c 46 | end 47 | return hosts, clusters 48 | end 49 | 50 | def get_valid_hosts(graph, cluster=nil) 51 | if @valid_hosts_table[[graph, cluster]] 52 | return @valid_hosts_table[[graph, cluster]] 53 | end 54 | 55 | clusters = Set.new 56 | if cluster 57 | hosts = Host.find_by_cluster(cluster) 58 | else 59 | hosts = Host.all 60 | end 61 | 62 | # filter as: 63 | # - the dashboard graph hosts definition 64 | # - the dashboard hosts definition 65 | # - the graph hosts definition 66 | # this is new behavior: before the filters were additive 67 | filter = graph_opts[graph]["hosts"] || @params["hosts"] || graph["hosts"] 68 | if filter 69 | hosts = hosts.select { |h| h.multi_match(filter) } 70 | end 71 | 72 | hosts.each { |h| clusters << h.cluster } 73 | 74 | @valid_hosts_table[[graph, cluster]] = [hosts, clusters] 75 | return hosts, clusters 76 | end 77 | 78 | def render_cluster_graph(graph, clusters, opts={}) 79 | # FIXME: edge case where the dash filter does not filter to a subset of 80 | # the hosts filter 81 | 82 | hosts = get_host_wildcards(graph) 83 | 84 | # graphite doesn't support strict matching (as /\d+/), so we need to 85 | # enumerate the hosts if a "#" wildcard is found 86 | if ! (filter = hosts.select { |h| h =~ /#/ }).empty? 87 | hosts_new = hosts - filter 88 | hosts2 = Host.all.select { |h| h.multi_match(filter) } 89 | hosts = (hosts2.map {|h| h.name } + hosts_new).sort.uniq.join(',') 90 | end 91 | 92 | opts[:sum] = :cluster unless opts[:zoom] 93 | graph_url = graph.render_url(hosts.to_a, clusters, opts) 94 | return graph_url 95 | end 96 | 97 | def get_host_wildcards(graph) 98 | return graph_opts[graph]["hosts"] || @params["hosts"] || graph["hosts"] 99 | end 100 | 101 | def render_global_graph(graph, opts={}) 102 | hosts = get_host_wildcards(graph) 103 | _, clusters = get_valid_hosts(graph) 104 | 105 | type = opts[:zoom] ? :cluster : :global 106 | options = opts.merge({:sum => type}) 107 | graph_url = graph.render_url(hosts, clusters, options) 108 | return graph_url 109 | end 110 | 111 | def self.find_by_graph(graph) 112 | ret = [] 113 | Dashboard.each do |name, dash| 114 | 115 | if dash["graphs"].map { |x| x.keys.first }.member?(graph.name) 116 | ret << dash 117 | end 118 | end 119 | 120 | return ret 121 | end 122 | end # Pencil::Models::Dashboard 123 | end 124 | end # Pencil::Models 125 | -------------------------------------------------------------------------------- /lib/pencil/models/graph.rb: -------------------------------------------------------------------------------- 1 | require "pencil/models/base" 2 | require "uri" 3 | 4 | module Pencil 5 | module Models 6 | class Graph < Base 7 | def initialize(name, params={}) 8 | super 9 | 10 | @params["hosts"] ||= ["*"] 11 | @params["title"] ||= name 12 | 13 | if not @params["targets"] 14 | raise ArgumentError, "graph #{name} needs a 'targets' map" 15 | end 16 | end 17 | 18 | def width(opts={}) 19 | opts["width"] || @params[:url_opts][:width] 20 | end 21 | 22 | # translate STR into graphite-speak for applying FUNC to STR 23 | # graphite functions take zero or one argument 24 | # pass passes STR through, instead of raising an error if FUNC isn't 25 | # recognized 26 | def translate(func, str, arg=nil, pass=false) 27 | # puts "calling translate" 28 | # puts "func => #{func}" 29 | # puts "str => #{str}" 30 | # puts "arg => #{arg}" 31 | # procs and lambdas don't support default arguments in 1.8, so I have to 32 | # do this 33 | z = lambda { |*body| "#{func}(#{body[0]||str})" } 34 | y = "#{str}, #{arg}" 35 | x = lambda { z.call(y) } 36 | 37 | return \ 38 | case func.to_s 39 | # comb 40 | when "sumSeries", "averageSeries", "minSeries", "maxSeries", "group" 41 | z.call 42 | # transform 43 | when "scale", "offset" 44 | # perhaps .to_f 45 | x.call 46 | when "derivative", "integral" 47 | z.call 48 | when "nonNegativeDerivative" 49 | z.call("#{str}#{', ' + arg if arg}") 50 | when "log", "timeShift", "summarize", "hitcount", 51 | # calculate 52 | "movingAverage", "stdev", "asPercent" 53 | x.call 54 | when "diffSeries", "divideSeries" 55 | z.call 56 | # filters 57 | when "mostDeviant" 58 | z.call("#{arg}, #{str}") 59 | when "highestCurrent", "lowestCurrent", "nPercentile", "currentAbove", 60 | "currentBelow", "highestAverage", "lowestAverage", "averageAbove", 61 | "averageBelow", "maximumAbove", "maximumBelow" 62 | x.call 63 | when "sortByMaxima", "minimalist" 64 | z.call 65 | when "limit", "exclude" 66 | x.call 67 | when "key", "alias" 68 | "alias(#{str}, \"#{arg}\")" 69 | when "cumulative", "drawAsInfinite" 70 | z.call 71 | when "lineWidth" 72 | x.call 73 | when "dashed", "keepLastValue" 74 | z.call 75 | when "substr", "threshold" 76 | x.call 77 | when "color" 78 | @params[:use_color] ? "color(#{str}, \"#{arg}\")" : str 79 | else 80 | raise "BAD FUNC #{func}" unless pass 81 | str 82 | end 83 | end 84 | 85 | # FIXME Map().keys => Array of strings 86 | # inner means we're dealing with a complex key; @params will be applied 87 | # make sure to apply alias and color arguments last, if applicable 88 | def handle_metric(name, opts, inner=false) 89 | later = [] 90 | ret = name.dup 91 | if inner 92 | @params.each do |k, v| 93 | ret = translate(k, ret, v, true) 94 | end 95 | end 96 | (opts||{}).each do |k, v| 97 | if k == "color" || k == "key" 98 | later << [k, v] 99 | else 100 | ret = translate(k, ret, v) 101 | end 102 | end 103 | later.each do |k, v| 104 | ret = translate(k, ret, v) 105 | end 106 | ret 107 | end 108 | 109 | def render_url(hosts, clusters, opts={}) 110 | opts = { 111 | :sum => nil, 112 | :title => @params["title"], 113 | }.merge(opts) 114 | 115 | if ! [:global, :cluster, nil].member?(opts[:sum]) 116 | raise ArgumentError, "render graph #{name}: invalid :sum - #{opts[:sum]}" 117 | end 118 | 119 | # FIXME actually use Maps for everything initially instead of this crap 120 | url_opts = Map({ 121 | :title => opts[:title], 122 | }).merge(Map(@params[:url_opts])).merge(Map(opts[:dynamic_url_opts])) 123 | 124 | url_opts[:from] = url_opts.delete(:stime) || "" 125 | url_opts[:until] = url_opts.delete(:etime) || "" 126 | url_opts.delete(:start) 127 | url_opts.delete(:duration) 128 | 129 | graphite_opts = [ "vtitle", "yMin", "yMax", "lineWidth", "areaMode", 130 | "template", "lineMode", "bgcolor", "graphOnly", "hideAxes", "hideGrid", 131 | "hideLegend", "fgcolor", "fontSize", "fontName", "fontItalic", 132 | "fontBold", "logBase" ] 133 | 134 | @params.each do |k, v| 135 | if graphite_opts.member?(k) 136 | url_opts[k.to_sym] = v 137 | end 138 | end 139 | 140 | target = [] 141 | colors = [] 142 | #FIXME code duplication 143 | if opts[:sum] == :global 144 | @params["targets"].each do |stat_name, o| 145 | z = new_map(o) 146 | 147 | z[:key] ||= stat_name 148 | ####################### 149 | if stat_name.instance_of?(Array) 150 | metric = stat_name.map do |m| 151 | mm = compose_metric(m.keys.first, 152 | "{#{clusters.to_a.join(',')}}", 153 | "{#{hosts.to_a.join(',')}}") 154 | 155 | if z.keys.member?("divideSeries") 156 | handle_metric(translate(:sumSeries, mm), 157 | m[m.keys.first], true) 158 | else 159 | handle_metric(mm, m[m.keys.first], true) 160 | end 161 | end.join(",") 162 | else 163 | metric = compose_metric(stat_name, 164 | "{#{clusters.to_a.join(',')}}", 165 | "{#{hosts.to_a.join(',')}}") 166 | metric = handle_metric(metric, {}, true) 167 | end 168 | ####################### 169 | z[:key] = "global #{z[:key]}" 170 | # target << handle_metric(translate(:sumSeries, metric), z) 171 | 172 | if z.keys.member?('divideSeries') # special case 173 | # apply divideSeries, sumSeries then other options 174 | res = translate(:divideSeries, metric) 175 | res = translate(:sumSeries, res) 176 | z.delete(:divideSeries) 177 | h = YAML::Omap.new 178 | z.each { |k,v| h[k] = v unless k == 'divideSeries' } 179 | target << handle_metric(res, h) 180 | else 181 | target << handle_metric(translate(:sumSeries, metric), z) 182 | end 183 | if !@params[:use_color] || 184 | (!z[:color] && @params[:use_color]) 185 | colors << next_color(colors, z[:color]) 186 | end 187 | end # @params["targets"].each 188 | elsif opts[:sum] == :cluster # one line per cluster/metric 189 | clusters.each do |cluster| 190 | @params["targets"].each do |stat_name, o| 191 | z = new_map(o) 192 | 193 | metrics = [] 194 | ####################### 195 | h = "{#{hosts.to_a.join(',')}}" 196 | if stat_name.instance_of?(Array) 197 | metrics << stat_name.map do |m| 198 | mm = compose_metric(m.keys.first, cluster, h) 199 | # note: we take the ratio of the sums in this case, instead of 200 | # the sums of the ratios 201 | if z.keys.member?('divideSeries') 202 | # divideSeries is picky about the number of series given as 203 | # arguments, so sum them in this case 204 | handle_metric(translate(:sumSeries, mm), 205 | m[m.keys.first], true) 206 | else 207 | handle_metric(mm, m[m.keys.first], true) 208 | end 209 | end.join(",") 210 | else 211 | metrics << handle_metric(compose_metric(stat_name, 212 | cluster, h), {}, true) 213 | end 214 | ####################### 215 | z[:key] = "#{cluster} #{z[:key]}" 216 | 217 | if z.keys.member?('divideSeries') # special case 218 | # apply divideSeries, sumSeries then other options 219 | res = translate(:divideSeries, metrics.join(',')) 220 | res = translate(:sumSeries, res) 221 | z.delete(:divideSeries) 222 | h = Map.new 223 | z.each { |k,v| h[k] = v unless k == 'divideSeries' } 224 | target << handle_metric(res, h) 225 | else 226 | target << handle_metric(translate(:sumSeries, 227 | metrics.join(',')), z) 228 | end 229 | 230 | if !@params[:use_color] || (!z[:color] && @params[:use_color]) 231 | colors << next_color(colors, z[:color]) 232 | end 233 | end # metrics.each 234 | end # clusters.each 235 | else # one line per {metric,host,colo} 236 | @params["targets"].each do |stat_name, o| 237 | z = new_map(o) 238 | clusters.each do |cluster| 239 | hosts.each do |host| 240 | label = "#{host} #{z[:key]}" 241 | ################# 242 | if stat_name.instance_of?(Array) 243 | metric = stat_name.map do |m| 244 | mm = compose_metric(m.keys.first, cluster, host) 245 | handle_metric(mm, m[m.keys.first], true) 246 | end.join(",") 247 | else 248 | metric = handle_metric(compose_metric(stat_name, cluster, host), {}, true) 249 | end 250 | ################# 251 | 252 | if label =~ /\*/ 253 | # for this particular type of graph, don't display a legend, 254 | # and color with abandon 255 | url_opts[:hideLegend] = true 256 | 257 | z.delete(:color) 258 | # fixme proper labeling... maybe 259 | # With wildcards let graphite construct the legend (or not). 260 | # Since we're handling wildcards we don't know how many 261 | # hosts will match, so just put in the default color list. 262 | # technically we do know, so this can be fixed 263 | z.delete(:key) 264 | target << handle_metric(metric, z) 265 | colors.concat(@params[:default_colors]) if colors.empty? 266 | else 267 | z[:key] = "#{host}/#{cluster} #{z[:key]}" 268 | target << handle_metric(metric, z) 269 | if !@params[:use_color] || 270 | (!z[:color] && @params[:use_color]) 271 | colors << next_color(colors, z[:color]) 272 | end 273 | end 274 | end 275 | end 276 | end # @params["targets"].each 277 | end # if opts[:sum] 278 | 279 | url_opts[:target] = target 280 | url_opts[:colorList] = colors.join(",") 281 | 282 | url = URI.join(@params[:graphite_url], "/render/?").to_s 283 | url_parts = [] 284 | url_opts.each do |k, v| 285 | [v].flatten.each do |v2| 286 | url_parts << "#{URI.escape(k.to_s)}=#{URI.escape(v2.to_s)}" 287 | end 288 | end 289 | url += url_parts.join("&") 290 | return url 291 | end 292 | 293 | # return an array of all metrics matching the specifications in 294 | # @params["targets"] 295 | # metrics are arrays of fields (once delimited by periods) 296 | def expand 297 | url = URI.join(@params[:graphite_url], "/metrics/expand/?query=").to_s 298 | metrics = [] 299 | 300 | require "json" 301 | @params["targets"].each do |k, v| 302 | # temporary hack for Map class complaining block parameters 303 | metric = [k, v] 304 | unless metric.first.instance_of?(Array) 305 | # wrap it 306 | metric[0] = [{metric[0] => nil}] 307 | end 308 | metric.first.each do |m| 309 | composed = compose_metric(m.first.first, "*", "*") 310 | composed2 = compose_metric2(m.first.first, "*", "*") 311 | query = open("#{url}#{composed}").read 312 | query2 = open("#{url}#{composed2}").read 313 | results = JSON.parse(query)["results"] 314 | results2 = JSON.parse(query2)["results"].map {|x| x.split('.')[0..-2].join('.')} 315 | metrics << results - results2 316 | end 317 | end 318 | 319 | return metrics.flatten.map { |x| x.split(".") } 320 | end 321 | 322 | def hosts_clusters 323 | metrics = expand 324 | clusters = Set.new 325 | 326 | # find the indicies of the clusters and hosts 327 | f = @params[:metric_format].dup.split("%m") 328 | first = f.first.split(".") 329 | last = f.last.split(".") 330 | ci = hi = nil 331 | first.each_with_index do |v, i| 332 | ci = i if v.match("%c") 333 | hi = i if v.match("%h") 334 | end 335 | unless ci && hi 336 | last.reverse.each_with_index do |v, i| 337 | ci = (-1) -i if v.match("%c") 338 | hi = (-1) -i if v.match("%h") 339 | end 340 | end 341 | hosts = metrics.map do |m| 342 | Host.new(m[hi], m[ci], @params) 343 | end.uniq 344 | 345 | # filter by what matches the graph definition 346 | hosts = hosts.select { |h| h.multi_match(@params["hosts"]) } 347 | hosts.each { |h| clusters << h.cluster } 348 | 349 | return hosts, clusters 350 | end 351 | 352 | private 353 | def next_color(colors, preferred_color=nil) 354 | default_colors = @params[:default_colors].clone 355 | 356 | if preferred_color and !colors.member?(preferred_color) 357 | return preferred_color 358 | end 359 | 360 | if preferred_color and ! default_colors.member?(preferred_color) 361 | default_colors << preferred_color 362 | end 363 | 364 | weights = Hash.new(0) 365 | colors.each do |c| 366 | weights[c] += 1 367 | end 368 | 369 | i = 0 370 | loop do 371 | default_colors.each do |c| 372 | return c if weights[c] == i 373 | end 374 | i += 1 375 | end 376 | end 377 | 378 | # if Map() in config.rb converted a YAML::Omap into an array, we 379 | # need to turn it back into a Map (which is ordered, conveniently) 380 | def new_map (opts) 381 | if opts.is_a?(Array) 382 | z = Map(opts.flatten) 383 | else 384 | z = Map(opts) 385 | end 386 | end 387 | end # Dash::Models::Graph 388 | end 389 | end # Pencil::Models 390 | -------------------------------------------------------------------------------- /lib/pencil/models/host.rb: -------------------------------------------------------------------------------- 1 | require "pencil/models/base" 2 | require "pencil/models/graph" 3 | 4 | module Pencil 5 | module Models 6 | class Host < Base 7 | attr_accessor :graphs 8 | attr_reader :cluster 9 | 10 | def initialize(name, cluster, params={}) 11 | super(name, params) 12 | @cluster = cluster 13 | # hack for the case where colo{1,2}.host1 both exist 14 | @@objects[self.class.to_s].delete(name) 15 | @@objects[self.class.to_s]["#{cluster}#{name}"] = self 16 | 17 | @graphs = [] 18 | Graph.each do |graph_name, graph| 19 | graph["hosts"].each do |h| 20 | if match(h) 21 | @graphs << graph 22 | break 23 | end 24 | end # graph["hosts"].each 25 | end # Graph.each 26 | @graphs.sort! 27 | end 28 | 29 | def self.find(name) 30 | return @@objects[self.name][key] rescue [] 31 | end 32 | 33 | def key 34 | "#{@cluster}#{@name}" 35 | end 36 | 37 | def eql?(other) 38 | key == other.key 39 | end 40 | 41 | def ==(other) 42 | key == other.key 43 | end 44 | 45 | def <=>(other) 46 | if @params[:host_sort] == "builtin" 47 | return key <=> other.key 48 | elsif @params[:host_sort] == "numeric" 49 | regex = /\d+/ 50 | match = @name.match(regex) 51 | match2 = other.name.match(regex) 52 | if match.pre_match != match2.pre_match 53 | return match.pre_match <=> match2.pre_match 54 | else 55 | return match[0].to_i <=> match2[0].to_i 56 | end 57 | else 58 | # http://www.bofh.org.uk/2007/12/16/comprehensible-sorting-in-ruby 59 | sensible = lambda do |k| 60 | k.to_s.split( 61 | /((?:(?:^|\s)[-+])?(?:\.\d+|\d+(?:\.\d+?(?:[eE]\d+)?(?:$|(?![eE\.])))?))/ms 62 | ).map { |v| Float(v) rescue v.downcase } 63 | end 64 | return sensible.call(self) <=> sensible.call(other) 65 | end 66 | end 67 | 68 | def hash 69 | key.hash 70 | end 71 | 72 | def self.find_by_name_and_cluster(name, cluster) 73 | Host.each do |host_name, host| 74 | next unless host.name == name 75 | return host if host.cluster == cluster 76 | end 77 | return nil 78 | end 79 | 80 | def self.find_by_cluster(cluster) 81 | ret = [] 82 | Host.each do |name, host| 83 | ret << host if host.cluster == cluster 84 | end 85 | return ret 86 | end 87 | 88 | end # Pencil::Models::Host 89 | end # Pencil::Models 90 | end 91 | -------------------------------------------------------------------------------- /lib/pencil/public/css/jquery_themes/pencil/images/ui-bg_flat_30_cccccc_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetep/pencil/0c4dc8fa4dfcfedb906982e6b32013522a09ac11/lib/pencil/public/css/jquery_themes/pencil/images/ui-bg_flat_30_cccccc_40x100.png -------------------------------------------------------------------------------- /lib/pencil/public/css/jquery_themes/pencil/images/ui-bg_flat_50_5c5c5c_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetep/pencil/0c4dc8fa4dfcfedb906982e6b32013522a09ac11/lib/pencil/public/css/jquery_themes/pencil/images/ui-bg_flat_50_5c5c5c_40x100.png -------------------------------------------------------------------------------- /lib/pencil/public/css/jquery_themes/pencil/images/ui-bg_glass_20_333333_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetep/pencil/0c4dc8fa4dfcfedb906982e6b32013522a09ac11/lib/pencil/public/css/jquery_themes/pencil/images/ui-bg_glass_20_333333_1x400.png -------------------------------------------------------------------------------- /lib/pencil/public/css/jquery_themes/pencil/images/ui-bg_glass_40_000000_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetep/pencil/0c4dc8fa4dfcfedb906982e6b32013522a09ac11/lib/pencil/public/css/jquery_themes/pencil/images/ui-bg_glass_40_000000_1x400.png -------------------------------------------------------------------------------- /lib/pencil/public/css/jquery_themes/pencil/images/ui-bg_glass_40_ffc73d_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetep/pencil/0c4dc8fa4dfcfedb906982e6b32013522a09ac11/lib/pencil/public/css/jquery_themes/pencil/images/ui-bg_glass_40_ffc73d_1x400.png -------------------------------------------------------------------------------- /lib/pencil/public/css/jquery_themes/pencil/images/ui-bg_gloss-wave_25_333333_500x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetep/pencil/0c4dc8fa4dfcfedb906982e6b32013522a09ac11/lib/pencil/public/css/jquery_themes/pencil/images/ui-bg_gloss-wave_25_333333_500x100.png -------------------------------------------------------------------------------- /lib/pencil/public/css/jquery_themes/pencil/images/ui-bg_highlight-soft_15_000000_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetep/pencil/0c4dc8fa4dfcfedb906982e6b32013522a09ac11/lib/pencil/public/css/jquery_themes/pencil/images/ui-bg_highlight-soft_15_000000_1x100.png -------------------------------------------------------------------------------- /lib/pencil/public/css/jquery_themes/pencil/images/ui-bg_highlight-soft_80_eeeeee_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetep/pencil/0c4dc8fa4dfcfedb906982e6b32013522a09ac11/lib/pencil/public/css/jquery_themes/pencil/images/ui-bg_highlight-soft_80_eeeeee_1x100.png -------------------------------------------------------------------------------- /lib/pencil/public/css/jquery_themes/pencil/images/ui-bg_inset-soft_30_ff4e0a_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetep/pencil/0c4dc8fa4dfcfedb906982e6b32013522a09ac11/lib/pencil/public/css/jquery_themes/pencil/images/ui-bg_inset-soft_30_ff4e0a_1x100.png -------------------------------------------------------------------------------- /lib/pencil/public/css/jquery_themes/pencil/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetep/pencil/0c4dc8fa4dfcfedb906982e6b32013522a09ac11/lib/pencil/public/css/jquery_themes/pencil/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /lib/pencil/public/css/jquery_themes/pencil/images/ui-icons_4b8e0b_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetep/pencil/0c4dc8fa4dfcfedb906982e6b32013522a09ac11/lib/pencil/public/css/jquery_themes/pencil/images/ui-icons_4b8e0b_256x240.png -------------------------------------------------------------------------------- /lib/pencil/public/css/jquery_themes/pencil/images/ui-icons_a83300_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetep/pencil/0c4dc8fa4dfcfedb906982e6b32013522a09ac11/lib/pencil/public/css/jquery_themes/pencil/images/ui-icons_a83300_256x240.png -------------------------------------------------------------------------------- /lib/pencil/public/css/jquery_themes/pencil/images/ui-icons_cccccc_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetep/pencil/0c4dc8fa4dfcfedb906982e6b32013522a09ac11/lib/pencil/public/css/jquery_themes/pencil/images/ui-icons_cccccc_256x240.png -------------------------------------------------------------------------------- /lib/pencil/public/css/jquery_themes/pencil/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetep/pencil/0c4dc8fa4dfcfedb906982e6b32013522a09ac11/lib/pencil/public/css/jquery_themes/pencil/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /lib/pencil/public/css/jquery_themes/pencil/jquery-ui-1.8.16.custom.css: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery UI CSS Framework 1.8.16 3 | * 4 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 5 | * Dual licensed under the MIT or GPL Version 2 licenses. 6 | * http://jquery.org/license 7 | * 8 | * http://docs.jquery.com/UI/Theming/API 9 | */ 10 | 11 | /* Layout helpers 12 | ----------------------------------*/ 13 | .ui-helper-hidden { display: none; } 14 | .ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); } 15 | .ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; } 16 | .ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } 17 | .ui-helper-clearfix { display: inline-block; } 18 | /* required comment for clearfix to work in Opera \*/ 19 | * html .ui-helper-clearfix { height:1%; } 20 | .ui-helper-clearfix { display:block; } 21 | /* end clearfix */ 22 | .ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); } 23 | 24 | 25 | /* Interaction Cues 26 | ----------------------------------*/ 27 | .ui-state-disabled { cursor: default !important; } 28 | 29 | 30 | /* Icons 31 | ----------------------------------*/ 32 | 33 | /* states and images */ 34 | .ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; } 35 | 36 | 37 | /* Misc visuals 38 | ----------------------------------*/ 39 | 40 | /* Overlays */ 41 | .ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } 42 | 43 | 44 | /* 45 | * jQuery UI CSS Framework 1.8.16 46 | * 47 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 48 | * Dual licensed under the MIT or GPL Version 2 licenses. 49 | * http://jquery.org/license 50 | * 51 | * http://docs.jquery.com/UI/Theming/API 52 | * 53 | * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Segoe%20UI,%20Arial,%20sans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=6px&bgColorHeader=333333&bgTextureHeader=12_gloss_wave.png&bgImgOpacityHeader=25&borderColorHeader=333333&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=000000&bgTextureContent=03_highlight_soft.png&bgImgOpacityContent=15&borderColorContent=666666&fcContent=ffffff&iconColorContent=cccccc&bgColorDefault=333333&bgTextureDefault=02_glass.png&bgImgOpacityDefault=20&borderColorDefault=666666&fcDefault=eeeeee&iconColorDefault=cccccc&bgColorHover=000000&bgTextureHover=02_glass.png&bgImgOpacityHover=40&borderColorHover=ffaf0f&fcHover=ffffff&iconColorHover=ffffff&bgColorActive=ff4e0a&bgTextureActive=05_inset_soft.png&bgImgOpacityActive=30&borderColorActive=ffaf0f&fcActive=ffffff&iconColorActive=222222&bgColorHighlight=eeeeee&bgTextureHighlight=03_highlight_soft.png&bgImgOpacityHighlight=80&borderColorHighlight=cccccc&fcHighlight=2e7db2&iconColorHighlight=4b8e0b&bgColorError=ffc73d&bgTextureError=02_glass.png&bgImgOpacityError=40&borderColorError=ffb73d&fcError=111111&iconColorError=a83300&bgColorOverlay=5c5c5c&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=50&opacityOverlay=80&bgColorShadow=cccccc&bgTextureShadow=01_flat.png&bgImgOpacityShadow=30&opacityShadow=60&thicknessShadow=7px&offsetTopShadow=-7px&offsetLeftShadow=-7px&cornerRadiusShadow=8px 54 | */ 55 | 56 | 57 | /* Component containers 58 | ----------------------------------*/ 59 | .ui-widget { font-family: Segoe UI, Arial, sans-serif; font-size: 1.1em; } 60 | .ui-widget .ui-widget { font-size: 1em; } 61 | .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Segoe UI, Arial, sans-serif; font-size: 1em; } 62 | .ui-widget-content { border: 1px solid #666666; background: #000000 url(images/ui-bg_highlight-soft_15_000000_1x100.png) 50% top repeat-x; color: #ffffff; } 63 | .ui-widget-content a { color: #ffffff; } 64 | .ui-widget-header { border: 1px solid #333333; background: #333333 url(images/ui-bg_gloss-wave_25_333333_500x100.png) 50% 50% repeat-x; color: #ffffff; font-weight: bold; } 65 | .ui-widget-header a { color: #ffffff; } 66 | 67 | /* Interaction states 68 | ----------------------------------*/ 69 | .ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #666666; background: #333333 url(images/ui-bg_glass_20_333333_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #eeeeee; } 70 | .ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #eeeeee; text-decoration: none; } 71 | .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 #ffaf0f; background: #000000 url(images/ui-bg_glass_40_000000_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #ffffff; } 72 | .ui-state-hover a, .ui-state-hover a:hover { color: #ffffff; text-decoration: none; } 73 | .ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #ffaf0f; background: #ff4e0a url(images/ui-bg_inset-soft_30_ff4e0a_1x100.png) 50% 50% repeat-x; font-weight: bold; color: #ffffff; } 74 | .ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #ffffff; text-decoration: none; } 75 | .ui-widget :active { outline: none; } 76 | 77 | /* Interaction Cues 78 | ----------------------------------*/ 79 | .ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #cccccc; background: #eeeeee url(images/ui-bg_highlight-soft_80_eeeeee_1x100.png) 50% top repeat-x; color: #2e7db2; } 80 | .ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #2e7db2; } 81 | .ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #ffb73d; background: #ffc73d url(images/ui-bg_glass_40_ffc73d_1x400.png) 50% 50% repeat-x; color: #111111; } 82 | .ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #111111; } 83 | .ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #111111; } 84 | .ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; } 85 | .ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; } 86 | .ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; } 87 | 88 | /* Icons 89 | ----------------------------------*/ 90 | 91 | /* states and images */ 92 | .ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_cccccc_256x240.png); } 93 | .ui-widget-content .ui-icon {background-image: url(images/ui-icons_cccccc_256x240.png); } 94 | .ui-widget-header .ui-icon {background-image: url(images/ui-icons_ffffff_256x240.png); } 95 | .ui-state-default .ui-icon { background-image: url(images/ui-icons_cccccc_256x240.png); } 96 | .ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_ffffff_256x240.png); } 97 | .ui-state-active .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); } 98 | .ui-state-highlight .ui-icon {background-image: url(images/ui-icons_4b8e0b_256x240.png); } 99 | .ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_a83300_256x240.png); } 100 | 101 | /* positioning */ 102 | .ui-icon-carat-1-n { background-position: 0 0; } 103 | .ui-icon-carat-1-ne { background-position: -16px 0; } 104 | .ui-icon-carat-1-e { background-position: -32px 0; } 105 | .ui-icon-carat-1-se { background-position: -48px 0; } 106 | .ui-icon-carat-1-s { background-position: -64px 0; } 107 | .ui-icon-carat-1-sw { background-position: -80px 0; } 108 | .ui-icon-carat-1-w { background-position: -96px 0; } 109 | .ui-icon-carat-1-nw { background-position: -112px 0; } 110 | .ui-icon-carat-2-n-s { background-position: -128px 0; } 111 | .ui-icon-carat-2-e-w { background-position: -144px 0; } 112 | .ui-icon-triangle-1-n { background-position: 0 -16px; } 113 | .ui-icon-triangle-1-ne { background-position: -16px -16px; } 114 | .ui-icon-triangle-1-e { background-position: -32px -16px; } 115 | .ui-icon-triangle-1-se { background-position: -48px -16px; } 116 | .ui-icon-triangle-1-s { background-position: -64px -16px; } 117 | .ui-icon-triangle-1-sw { background-position: -80px -16px; } 118 | .ui-icon-triangle-1-w { background-position: -96px -16px; } 119 | .ui-icon-triangle-1-nw { background-position: -112px -16px; } 120 | .ui-icon-triangle-2-n-s { background-position: -128px -16px; } 121 | .ui-icon-triangle-2-e-w { background-position: -144px -16px; } 122 | .ui-icon-arrow-1-n { background-position: 0 -32px; } 123 | .ui-icon-arrow-1-ne { background-position: -16px -32px; } 124 | .ui-icon-arrow-1-e { background-position: -32px -32px; } 125 | .ui-icon-arrow-1-se { background-position: -48px -32px; } 126 | .ui-icon-arrow-1-s { background-position: -64px -32px; } 127 | .ui-icon-arrow-1-sw { background-position: -80px -32px; } 128 | .ui-icon-arrow-1-w { background-position: -96px -32px; } 129 | .ui-icon-arrow-1-nw { background-position: -112px -32px; } 130 | .ui-icon-arrow-2-n-s { background-position: -128px -32px; } 131 | .ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } 132 | .ui-icon-arrow-2-e-w { background-position: -160px -32px; } 133 | .ui-icon-arrow-2-se-nw { background-position: -176px -32px; } 134 | .ui-icon-arrowstop-1-n { background-position: -192px -32px; } 135 | .ui-icon-arrowstop-1-e { background-position: -208px -32px; } 136 | .ui-icon-arrowstop-1-s { background-position: -224px -32px; } 137 | .ui-icon-arrowstop-1-w { background-position: -240px -32px; } 138 | .ui-icon-arrowthick-1-n { background-position: 0 -48px; } 139 | .ui-icon-arrowthick-1-ne { background-position: -16px -48px; } 140 | .ui-icon-arrowthick-1-e { background-position: -32px -48px; } 141 | .ui-icon-arrowthick-1-se { background-position: -48px -48px; } 142 | .ui-icon-arrowthick-1-s { background-position: -64px -48px; } 143 | .ui-icon-arrowthick-1-sw { background-position: -80px -48px; } 144 | .ui-icon-arrowthick-1-w { background-position: -96px -48px; } 145 | .ui-icon-arrowthick-1-nw { background-position: -112px -48px; } 146 | .ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } 147 | .ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } 148 | .ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } 149 | .ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } 150 | .ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } 151 | .ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } 152 | .ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } 153 | .ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } 154 | .ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } 155 | .ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } 156 | .ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } 157 | .ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } 158 | .ui-icon-arrowreturn-1-w { background-position: -64px -64px; } 159 | .ui-icon-arrowreturn-1-n { background-position: -80px -64px; } 160 | .ui-icon-arrowreturn-1-e { background-position: -96px -64px; } 161 | .ui-icon-arrowreturn-1-s { background-position: -112px -64px; } 162 | .ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } 163 | .ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } 164 | .ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } 165 | .ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } 166 | .ui-icon-arrow-4 { background-position: 0 -80px; } 167 | .ui-icon-arrow-4-diag { background-position: -16px -80px; } 168 | .ui-icon-extlink { background-position: -32px -80px; } 169 | .ui-icon-newwin { background-position: -48px -80px; } 170 | .ui-icon-refresh { background-position: -64px -80px; } 171 | .ui-icon-shuffle { background-position: -80px -80px; } 172 | .ui-icon-transfer-e-w { background-position: -96px -80px; } 173 | .ui-icon-transferthick-e-w { background-position: -112px -80px; } 174 | .ui-icon-folder-collapsed { background-position: 0 -96px; } 175 | .ui-icon-folder-open { background-position: -16px -96px; } 176 | .ui-icon-document { background-position: -32px -96px; } 177 | .ui-icon-document-b { background-position: -48px -96px; } 178 | .ui-icon-note { background-position: -64px -96px; } 179 | .ui-icon-mail-closed { background-position: -80px -96px; } 180 | .ui-icon-mail-open { background-position: -96px -96px; } 181 | .ui-icon-suitcase { background-position: -112px -96px; } 182 | .ui-icon-comment { background-position: -128px -96px; } 183 | .ui-icon-person { background-position: -144px -96px; } 184 | .ui-icon-print { background-position: -160px -96px; } 185 | .ui-icon-trash { background-position: -176px -96px; } 186 | .ui-icon-locked { background-position: -192px -96px; } 187 | .ui-icon-unlocked { background-position: -208px -96px; } 188 | .ui-icon-bookmark { background-position: -224px -96px; } 189 | .ui-icon-tag { background-position: -240px -96px; } 190 | .ui-icon-home { background-position: 0 -112px; } 191 | .ui-icon-flag { background-position: -16px -112px; } 192 | .ui-icon-calendar { background-position: -32px -112px; } 193 | .ui-icon-cart { background-position: -48px -112px; } 194 | .ui-icon-pencil { background-position: -64px -112px; } 195 | .ui-icon-clock { background-position: -80px -112px; } 196 | .ui-icon-disk { background-position: -96px -112px; } 197 | .ui-icon-calculator { background-position: -112px -112px; } 198 | .ui-icon-zoomin { background-position: -128px -112px; } 199 | .ui-icon-zoomout { background-position: -144px -112px; } 200 | .ui-icon-search { background-position: -160px -112px; } 201 | .ui-icon-wrench { background-position: -176px -112px; } 202 | .ui-icon-gear { background-position: -192px -112px; } 203 | .ui-icon-heart { background-position: -208px -112px; } 204 | .ui-icon-star { background-position: -224px -112px; } 205 | .ui-icon-link { background-position: -240px -112px; } 206 | .ui-icon-cancel { background-position: 0 -128px; } 207 | .ui-icon-plus { background-position: -16px -128px; } 208 | .ui-icon-plusthick { background-position: -32px -128px; } 209 | .ui-icon-minus { background-position: -48px -128px; } 210 | .ui-icon-minusthick { background-position: -64px -128px; } 211 | .ui-icon-close { background-position: -80px -128px; } 212 | .ui-icon-closethick { background-position: -96px -128px; } 213 | .ui-icon-key { background-position: -112px -128px; } 214 | .ui-icon-lightbulb { background-position: -128px -128px; } 215 | .ui-icon-scissors { background-position: -144px -128px; } 216 | .ui-icon-clipboard { background-position: -160px -128px; } 217 | .ui-icon-copy { background-position: -176px -128px; } 218 | .ui-icon-contact { background-position: -192px -128px; } 219 | .ui-icon-image { background-position: -208px -128px; } 220 | .ui-icon-video { background-position: -224px -128px; } 221 | .ui-icon-script { background-position: -240px -128px; } 222 | .ui-icon-alert { background-position: 0 -144px; } 223 | .ui-icon-info { background-position: -16px -144px; } 224 | .ui-icon-notice { background-position: -32px -144px; } 225 | .ui-icon-help { background-position: -48px -144px; } 226 | .ui-icon-check { background-position: -64px -144px; } 227 | .ui-icon-bullet { background-position: -80px -144px; } 228 | .ui-icon-radio-off { background-position: -96px -144px; } 229 | .ui-icon-radio-on { background-position: -112px -144px; } 230 | .ui-icon-pin-w { background-position: -128px -144px; } 231 | .ui-icon-pin-s { background-position: -144px -144px; } 232 | .ui-icon-play { background-position: 0 -160px; } 233 | .ui-icon-pause { background-position: -16px -160px; } 234 | .ui-icon-seek-next { background-position: -32px -160px; } 235 | .ui-icon-seek-prev { background-position: -48px -160px; } 236 | .ui-icon-seek-end { background-position: -64px -160px; } 237 | .ui-icon-seek-start { background-position: -80px -160px; } 238 | /* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ 239 | .ui-icon-seek-first { background-position: -80px -160px; } 240 | .ui-icon-stop { background-position: -96px -160px; } 241 | .ui-icon-eject { background-position: -112px -160px; } 242 | .ui-icon-volume-off { background-position: -128px -160px; } 243 | .ui-icon-volume-on { background-position: -144px -160px; } 244 | .ui-icon-power { background-position: 0 -176px; } 245 | .ui-icon-signal-diag { background-position: -16px -176px; } 246 | .ui-icon-signal { background-position: -32px -176px; } 247 | .ui-icon-battery-0 { background-position: -48px -176px; } 248 | .ui-icon-battery-1 { background-position: -64px -176px; } 249 | .ui-icon-battery-2 { background-position: -80px -176px; } 250 | .ui-icon-battery-3 { background-position: -96px -176px; } 251 | .ui-icon-circle-plus { background-position: 0 -192px; } 252 | .ui-icon-circle-minus { background-position: -16px -192px; } 253 | .ui-icon-circle-close { background-position: -32px -192px; } 254 | .ui-icon-circle-triangle-e { background-position: -48px -192px; } 255 | .ui-icon-circle-triangle-s { background-position: -64px -192px; } 256 | .ui-icon-circle-triangle-w { background-position: -80px -192px; } 257 | .ui-icon-circle-triangle-n { background-position: -96px -192px; } 258 | .ui-icon-circle-arrow-e { background-position: -112px -192px; } 259 | .ui-icon-circle-arrow-s { background-position: -128px -192px; } 260 | .ui-icon-circle-arrow-w { background-position: -144px -192px; } 261 | .ui-icon-circle-arrow-n { background-position: -160px -192px; } 262 | .ui-icon-circle-zoomin { background-position: -176px -192px; } 263 | .ui-icon-circle-zoomout { background-position: -192px -192px; } 264 | .ui-icon-circle-check { background-position: -208px -192px; } 265 | .ui-icon-circlesmall-plus { background-position: 0 -208px; } 266 | .ui-icon-circlesmall-minus { background-position: -16px -208px; } 267 | .ui-icon-circlesmall-close { background-position: -32px -208px; } 268 | .ui-icon-squaresmall-plus { background-position: -48px -208px; } 269 | .ui-icon-squaresmall-minus { background-position: -64px -208px; } 270 | .ui-icon-squaresmall-close { background-position: -80px -208px; } 271 | .ui-icon-grip-dotted-vertical { background-position: 0 -224px; } 272 | .ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } 273 | .ui-icon-grip-solid-vertical { background-position: -32px -224px; } 274 | .ui-icon-grip-solid-horizontal { background-position: -48px -224px; } 275 | .ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } 276 | .ui-icon-grip-diagonal-se { background-position: -80px -224px; } 277 | 278 | 279 | /* Misc visuals 280 | ----------------------------------*/ 281 | 282 | /* Corner radius */ 283 | .ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 6px; -webkit-border-top-left-radius: 6px; -khtml-border-top-left-radius: 6px; border-top-left-radius: 6px; } 284 | .ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 6px; -webkit-border-top-right-radius: 6px; -khtml-border-top-right-radius: 6px; border-top-right-radius: 6px; } 285 | .ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 6px; -webkit-border-bottom-left-radius: 6px; -khtml-border-bottom-left-radius: 6px; border-bottom-left-radius: 6px; } 286 | .ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 6px; -webkit-border-bottom-right-radius: 6px; -khtml-border-bottom-right-radius: 6px; border-bottom-right-radius: 6px; } 287 | 288 | /* Overlays */ 289 | .ui-widget-overlay { background: #5c5c5c url(images/ui-bg_flat_50_5c5c5c_40x100.png) 50% 50% repeat-x; opacity: .80;filter:Alpha(Opacity=80); } 290 | .ui-widget-shadow { margin: -7px 0 0 -7px; padding: 7px; background: #cccccc url(images/ui-bg_flat_30_cccccc_40x100.png) 50% 50% repeat-x; opacity: .60;filter:Alpha(Opacity=60); -moz-border-radius: 8px; -khtml-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }/* 291 | * jQuery UI Resizable 1.8.16 292 | * 293 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 294 | * Dual licensed under the MIT or GPL Version 2 licenses. 295 | * http://jquery.org/license 296 | * 297 | * http://docs.jquery.com/UI/Resizable#theming 298 | */ 299 | .ui-resizable { position: relative;} 300 | .ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block; } 301 | .ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; } 302 | .ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; } 303 | .ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; } 304 | .ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; } 305 | .ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; } 306 | .ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; } 307 | .ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; } 308 | .ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; } 309 | .ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/* 310 | * jQuery UI Selectable 1.8.16 311 | * 312 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 313 | * Dual licensed under the MIT or GPL Version 2 licenses. 314 | * http://jquery.org/license 315 | * 316 | * http://docs.jquery.com/UI/Selectable#theming 317 | */ 318 | .ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; } 319 | /* 320 | * jQuery UI Accordion 1.8.16 321 | * 322 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 323 | * Dual licensed under the MIT or GPL Version 2 licenses. 324 | * http://jquery.org/license 325 | * 326 | * http://docs.jquery.com/UI/Accordion#theming 327 | */ 328 | /* IE/Win - Fix animation bug - #4615 */ 329 | .ui-accordion { width: 100%; } 330 | .ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; } 331 | .ui-accordion .ui-accordion-li-fix { display: inline; } 332 | .ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; } 333 | .ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em .7em; } 334 | .ui-accordion-icons .ui-accordion-header a { padding-left: 2.2em; } 335 | .ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; } 336 | .ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; zoom: 1; } 337 | .ui-accordion .ui-accordion-content-active { display: block; } 338 | /* 339 | * jQuery UI Autocomplete 1.8.16 340 | * 341 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 342 | * Dual licensed under the MIT or GPL Version 2 licenses. 343 | * http://jquery.org/license 344 | * 345 | * http://docs.jquery.com/UI/Autocomplete#theming 346 | */ 347 | .ui-autocomplete { position: absolute; cursor: default; } 348 | 349 | /* workarounds */ 350 | * html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */ 351 | 352 | /* 353 | * jQuery UI Menu 1.8.16 354 | * 355 | * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 356 | * Dual licensed under the MIT or GPL Version 2 licenses. 357 | * http://jquery.org/license 358 | * 359 | * http://docs.jquery.com/UI/Menu#theming 360 | */ 361 | .ui-menu { 362 | list-style:none; 363 | padding: 2px; 364 | margin: 0; 365 | display:block; 366 | float: left; 367 | } 368 | .ui-menu .ui-menu { 369 | margin-top: -3px; 370 | } 371 | .ui-menu .ui-menu-item { 372 | margin:0; 373 | padding: 0; 374 | zoom: 1; 375 | float: left; 376 | clear: left; 377 | width: 100%; 378 | } 379 | .ui-menu .ui-menu-item a { 380 | text-decoration:none; 381 | display:block; 382 | padding:.2em .4em; 383 | line-height:1.5; 384 | zoom:1; 385 | } 386 | .ui-menu .ui-menu-item a.ui-state-hover, 387 | .ui-menu .ui-menu-item a.ui-state-active { 388 | font-weight: normal; 389 | margin: -1px; 390 | } 391 | /* 392 | * jQuery UI Button 1.8.16 393 | * 394 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 395 | * Dual licensed under the MIT or GPL Version 2 licenses. 396 | * http://jquery.org/license 397 | * 398 | * http://docs.jquery.com/UI/Button#theming 399 | */ 400 | .ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */ 401 | .ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */ 402 | button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */ 403 | .ui-button-icons-only { width: 3.4em; } 404 | button.ui-button-icons-only { width: 3.7em; } 405 | 406 | /*button text element */ 407 | .ui-button .ui-button-text { display: block; line-height: 1.4; } 408 | .ui-button-text-only .ui-button-text { padding: .4em 1em; } 409 | .ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; } 410 | .ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; } 411 | .ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; } 412 | .ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; } 413 | /* no icon support for input elements, provide padding by default */ 414 | input.ui-button { padding: .4em 1em; } 415 | 416 | /*button icon element(s) */ 417 | .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; } 418 | .ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; } 419 | .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; } 420 | .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; } 421 | .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } 422 | 423 | /*button sets*/ 424 | .ui-buttonset { margin-right: 7px; } 425 | .ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; } 426 | 427 | /* workarounds */ 428 | button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */ 429 | /* 430 | * jQuery UI Dialog 1.8.16 431 | * 432 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 433 | * Dual licensed under the MIT or GPL Version 2 licenses. 434 | * http://jquery.org/license 435 | * 436 | * http://docs.jquery.com/UI/Dialog#theming 437 | */ 438 | .ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; } 439 | .ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative; } 440 | .ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; } 441 | .ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; } 442 | .ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; } 443 | .ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; } 444 | .ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; } 445 | .ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; } 446 | .ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; } 447 | .ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; } 448 | .ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; } 449 | .ui-draggable .ui-dialog-titlebar { cursor: move; } 450 | /* 451 | * jQuery UI Slider 1.8.16 452 | * 453 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 454 | * Dual licensed under the MIT or GPL Version 2 licenses. 455 | * http://jquery.org/license 456 | * 457 | * http://docs.jquery.com/UI/Slider#theming 458 | */ 459 | .ui-slider { position: relative; text-align: left; } 460 | .ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; } 461 | .ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; } 462 | 463 | .ui-slider-horizontal { height: .8em; } 464 | .ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; } 465 | .ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; } 466 | .ui-slider-horizontal .ui-slider-range-min { left: 0; } 467 | .ui-slider-horizontal .ui-slider-range-max { right: 0; } 468 | 469 | .ui-slider-vertical { width: .8em; height: 100px; } 470 | .ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; } 471 | .ui-slider-vertical .ui-slider-range { left: 0; width: 100%; } 472 | .ui-slider-vertical .ui-slider-range-min { bottom: 0; } 473 | .ui-slider-vertical .ui-slider-range-max { top: 0; }/* 474 | * jQuery UI Tabs 1.8.16 475 | * 476 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 477 | * Dual licensed under the MIT or GPL Version 2 licenses. 478 | * http://jquery.org/license 479 | * 480 | * http://docs.jquery.com/UI/Tabs#theming 481 | */ 482 | .ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ 483 | .ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; } 484 | .ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap; } 485 | .ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; } 486 | .ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; } 487 | .ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; } 488 | .ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ 489 | .ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; } 490 | .ui-tabs .ui-tabs-hide { display: none !important; } 491 | /* 492 | * jQuery UI Datepicker 1.8.16 493 | * 494 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 495 | * Dual licensed under the MIT or GPL Version 2 licenses. 496 | * http://jquery.org/license 497 | * 498 | * http://docs.jquery.com/UI/Datepicker#theming 499 | */ 500 | .ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; } 501 | .ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; } 502 | .ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; } 503 | .ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; } 504 | .ui-datepicker .ui-datepicker-prev { left:2px; } 505 | .ui-datepicker .ui-datepicker-next { right:2px; } 506 | .ui-datepicker .ui-datepicker-prev-hover { left:1px; } 507 | .ui-datepicker .ui-datepicker-next-hover { right:1px; } 508 | .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; } 509 | .ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; } 510 | .ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; } 511 | .ui-datepicker select.ui-datepicker-month-year {width: 100%;} 512 | .ui-datepicker select.ui-datepicker-month, 513 | .ui-datepicker select.ui-datepicker-year { width: 49%;} 514 | .ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; } 515 | .ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; } 516 | .ui-datepicker td { border: 0; padding: 1px; } 517 | .ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; } 518 | .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; } 519 | .ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; } 520 | .ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; } 521 | 522 | /* with multiple calendars */ 523 | .ui-datepicker.ui-datepicker-multi { width:auto; } 524 | .ui-datepicker-multi .ui-datepicker-group { float:left; } 525 | .ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; } 526 | .ui-datepicker-multi-2 .ui-datepicker-group { width:50%; } 527 | .ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; } 528 | .ui-datepicker-multi-4 .ui-datepicker-group { width:25%; } 529 | .ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; } 530 | .ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; } 531 | .ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; } 532 | .ui-datepicker-row-break { clear:both; width:100%; font-size:0em; } 533 | 534 | /* RTL support */ 535 | .ui-datepicker-rtl { direction: rtl; } 536 | .ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; } 537 | .ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; } 538 | .ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; } 539 | .ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; } 540 | .ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; } 541 | .ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; } 542 | .ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; } 543 | .ui-datepicker-rtl .ui-datepicker-group { float:right; } 544 | .ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; } 545 | .ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; } 546 | 547 | /* IE6 IFRAME FIX (taken from datepicker 1.5.3 */ 548 | .ui-datepicker-cover { 549 | display: none; /*sorry for IE5*/ 550 | display/**/: block; /*sorry for IE5*/ 551 | position: absolute; /*must have*/ 552 | z-index: -1; /*must have*/ 553 | filter: mask(); /*must have*/ 554 | top: -4px; /*must have*/ 555 | left: -4px; /*must have*/ 556 | width: 200px; /*must have*/ 557 | height: 200px; /*must have*/ 558 | }/* 559 | * jQuery UI Progressbar 1.8.16 560 | * 561 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 562 | * Dual licensed under the MIT or GPL Version 2 licenses. 563 | * http://jquery.org/license 564 | * 565 | * http://docs.jquery.com/UI/Progressbar#theming 566 | */ 567 | .ui-progressbar { height:2em; text-align: left; } 568 | .ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; } -------------------------------------------------------------------------------- /lib/pencil/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetep/pencil/0c4dc8fa4dfcfedb906982e6b32013522a09ac11/lib/pencil/public/favicon.ico -------------------------------------------------------------------------------- /lib/pencil/public/js/cufon.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2009 Simo Kinnunen. 3 | * Licensed under the MIT license. 4 | * 5 | * @version 1.09i 6 | */ 7 | var Cufon=(function(){var m=function(){return m.replace.apply(null,arguments)};var x=m.DOM={ready:(function(){var C=false,E={loaded:1,complete:1};var B=[],D=function(){if(C){return}C=true;for(var F;F=B.shift();F()){}};if(document.addEventListener){document.addEventListener("DOMContentLoaded",D,false);window.addEventListener("pageshow",D,false)}if(!window.opera&&document.readyState){(function(){E[document.readyState]?D():setTimeout(arguments.callee,10)})()}if(document.readyState&&document.createStyleSheet){(function(){try{document.body.doScroll("left");D()}catch(F){setTimeout(arguments.callee,1)}})()}q(window,"load",D);return function(F){if(!arguments.length){D()}else{C?F():B.push(F)}}})(),root:function(){return document.documentElement||document.body}};var n=m.CSS={Size:function(C,B){this.value=parseFloat(C);this.unit=String(C).match(/[a-z%]*$/)[0]||"px";this.convert=function(D){return D/B*this.value};this.convertFrom=function(D){return D/this.value*B};this.toString=function(){return this.value+this.unit}},addClass:function(C,B){var D=C.className;C.className=D+(D&&" ")+B;return C},color:j(function(C){var B={};B.color=C.replace(/^rgba\((.*?),\s*([\d.]+)\)/,function(E,D,F){B.opacity=parseFloat(F);return"rgb("+D+")"});return B}),fontStretch:j(function(B){if(typeof B=="number"){return B}if(/%$/.test(B)){return parseFloat(B)/100}return{"ultra-condensed":0.5,"extra-condensed":0.625,condensed:0.75,"semi-condensed":0.875,"semi-expanded":1.125,expanded:1.25,"extra-expanded":1.5,"ultra-expanded":2}[B]||1}),getStyle:function(C){var B=document.defaultView;if(B&&B.getComputedStyle){return new a(B.getComputedStyle(C,null))}if(C.currentStyle){return new a(C.currentStyle)}return new a(C.style)},gradient:j(function(F){var G={id:F,type:F.match(/^-([a-z]+)-gradient\(/)[1],stops:[]},C=F.substr(F.indexOf("(")).match(/([\d.]+=)?(#[a-f0-9]+|[a-z]+\(.*?\)|[a-z]+)/ig);for(var E=0,B=C.length,D;E0){E=" "+E}}else{if(B400}if(I==500){I=400}for(var J in G){if(!k(G,J)){continue}J=parseInt(J,10);if(!F||JD){D=J}K.push(J)}if(ID){I=D}K.sort(function(M,L){return(E?(M>=I&&L>=I)?ML:(M<=I&&L<=I)?M>L:Mcufoncanvas{text-indent:0;}@media screen{cvml\\:shape,cvml\\:rect,cvml\\:fill,cvml\\:shadow{behavior:url(#default#VML);display:block;antialias:true;position:absolute;}cufoncanvas{position:absolute;text-align:left;}cufon{display:inline-block;position:relative;vertical-align:'+(h?"middle":"text-bottom")+";}cufon cufontext{position:absolute;left:-10000in;font-size:1px;}a cufon{cursor:pointer}}@media print{cufon cufoncanvas{display:none;}}").replace(/;/g,"!important;"));function c(i,j){return a(i,/(?:em|ex|%)$|^[a-z-]+$/i.test(j)?"1em":j)}function a(l,m){if(m==="0"){return 0}if(/px$/i.test(m)){return parseFloat(m)}var k=l.style.left,j=l.runtimeStyle.left;l.runtimeStyle.left=l.currentStyle.left;l.style.left=m.replace("%","em");var i=l.style.pixelLeft;l.style.left=k;l.runtimeStyle.left=j;return i}function f(l,k,j,n){var i="computed"+n,m=k[i];if(isNaN(m)){m=k.get(n);k[i]=m=(m=="normal")?0:~~j.convertFrom(a(l,m))}return m}var g={};function d(p){var q=p.id;if(!g[q]){var n=p.stops,o=document.createElement("cvml:fill"),i=[];o.type="gradient";o.angle=180;o.focus="0";o.method="sigma";o.color=n[0][1];for(var m=1,l=n.length-1;mO){O=K}if(I>N){N=I}if(K":{"d":"24,39v80,-182,296,-240,409,-389v-93,-42,-236,-79,-336,-112v-29,-34,-14,-97,33,-99v59,-4,125,43,171,59v67,23,207,47,251,93v-54,183,-235,263,-363,369v-39,32,-70,102,-132,112v1,-22,-18,-25,-33,-33","w":576},"?":{"d":"310,-574v-66,-44,-169,3,-194,58v-12,27,-22,56,-66,46v-10,-11,-14,-31,-20,-45v28,-39,50,-84,88,-113v66,-51,215,-51,261,20v-15,125,-82,195,-143,275v9,42,0,99,-14,128v-14,-6,-25,10,-35,10v-32,-26,-31,-88,-34,-143v60,-70,133,-129,157,-236xm232,-92v-8,39,19,126,-53,104v-11,-4,-19,-5,-26,-5v-34,-34,-21,-99,25,-107v15,-3,33,-1,54,8","w":424},"@":{"d":"412,-191v-7,-9,-50,-20,-56,-30v-36,27,-116,113,-172,48v-58,-42,-18,-138,16,-183v43,-56,116,-147,207,-98v66,36,-33,123,15,182v122,0,191,-89,222,-186v23,-72,11,-153,-24,-204v-131,-36,-259,17,-380,162v-66,79,-142,181,-158,303v-27,216,176,246,376,260v-3,55,-83,18,-112,45v-98,-3,-180,-15,-233,-55v-94,-69,-120,-222,-66,-353v66,-161,167,-291,302,-380v95,-63,206,-93,296,-30v30,21,47,52,56,89v41,183,-35,324,-228,425v-32,-1,-37,-2,-61,5xm336,-282v-9,-26,-4,-69,0,-96v-55,23,-109,81,-101,157v31,3,64,-18,101,-61","w":732},"A":{"d":"162,-161v42,-38,90,-92,171,-93v-1,-33,-73,-159,-78,-191v-126,100,-71,377,-238,443v-33,-103,21,-202,50,-278r101,-258v15,-41,28,-83,40,-128v74,-25,66,94,130,53v45,118,79,252,158,337v59,10,131,4,148,59v-28,13,-39,31,-53,60v21,35,39,61,54,80v2,51,-33,61,-68,79v-44,-18,-135,-142,-179,-160v-68,-10,-179,58,-227,65","w":656},"B":{"d":"31,-421v0,-13,-16,-40,-15,-54v40,-37,86,-58,137,-61v-6,-48,-1,-88,15,-119v18,-37,44,-38,77,-3v5,19,2,50,-8,92v89,7,191,0,229,61v9,67,-41,96,-61,137v50,1,226,-11,287,0v49,9,88,22,117,46v6,116,-104,160,-152,221v-16,6,-59,26,-130,57v-121,53,-186,84,-328,88v13,-78,-20,-152,-30,-214v-45,9,-88,9,-130,0v-36,-84,61,-98,114,-129v-3,-44,9,-119,-7,-153v-25,3,-89,29,-115,31xm237,-452r0,92v54,-4,127,-56,145,-99v-40,-24,-104,-11,-145,7xm252,-208v15,37,-14,134,-15,160v223,-48,378,-122,465,-221v-140,-39,-290,-19,-450,61","w":840},"C":{"d":"550,-632v13,83,-80,57,-149,70v-99,19,-146,121,-220,170v-61,182,23,344,216,294v141,-37,221,-154,383,-165v-51,106,-303,266,-449,280v-170,16,-281,-65,-311,-203v-43,-197,86,-325,202,-416v107,-84,188,-96,328,-30","w":817},"D":{"d":"194,-520v-33,165,-29,290,17,434v-38,68,-74,78,-106,28v-45,-70,-72,-240,-47,-374v15,-78,17,-99,-20,-140v-15,-16,-20,-31,-17,-43v8,1,6,-2,8,-9v66,-54,112,-63,139,-26v84,-10,274,62,338,70v41,5,185,119,208,138v46,38,78,123,78,200v0,34,-25,71,-60,113v-186,120,-366,226,-642,225r0,-69v33,-6,66,-12,98,-19v159,-37,341,-85,450,-174v72,-59,43,-183,-22,-229v-104,-74,-258,-110,-422,-125","w":836},"E":{"d":"373,-313v10,216,242,212,463,217v40,1,72,6,94,12v13,68,-58,70,-119,79v-103,14,-283,0,-338,-40v-21,-5,-34,30,-60,20v-125,-49,-195,-137,-208,-268v-75,-12,-103,49,-179,40v-3,-30,-11,-61,0,-90v12,-30,134,-42,149,-69v-16,-68,-60,-157,-20,-229v280,-26,559,25,755,120v-163,63,-426,-23,-586,39v15,41,47,42,98,30v26,-7,42,-10,51,-10v121,0,220,13,298,40v-47,48,-338,82,-398,109","w":920},"F":{"d":"917,-562v-248,3,-453,28,-619,108v-2,44,4,100,-9,132v110,-32,208,-104,355,-78v33,6,68,21,105,40v-27,49,-100,69,-220,62v-142,60,-124,49,-240,134v-8,70,12,166,-62,176v-27,-18,-39,-65,-35,-141v-6,-34,-37,4,-52,-17v-22,-61,29,-80,52,-106v-7,-59,-4,-129,9,-211v-64,13,-131,30,-205,9v-11,-24,-20,-58,-8,-88v44,-18,165,-11,213,-27v-14,-50,51,-122,101,-99v17,9,27,27,31,55v89,5,130,-10,205,-20v240,-31,366,-8,379,71","w":867},"G":{"d":"389,-656v7,58,10,108,8,149v-37,9,-65,2,-84,-19v-108,33,-186,148,-164,296v6,44,26,82,55,113v59,35,147,56,224,25v55,-22,109,-56,160,-81v-21,-45,-29,-89,-86,-76v-49,11,-119,13,-165,-9v-23,-11,-44,-16,-61,-17r0,-93v49,-36,104,-61,172,-46v90,20,189,45,302,38v81,-5,68,17,144,54v-29,19,-42,30,-41,35v-15,-3,-59,-2,-73,9v0,10,16,17,10,31v-16,-9,-73,5,-93,0v0,61,-3,99,-8,112v-110,91,-262,181,-463,152v-174,-25,-248,-186,-199,-413v27,-126,119,-231,241,-266v40,-11,81,-9,121,6","w":845},"H":{"d":"800,46v-130,-34,-165,-158,-190,-300v-63,61,-246,42,-341,70v-5,37,45,83,40,120v-55,36,-103,33,-142,-10v-35,-39,-57,-90,-68,-150v-60,-14,-112,-69,-80,-150v16,-17,60,-10,80,-20v-1,-80,-1,-130,2,-149v12,-74,43,-107,118,-101v47,67,23,216,40,310v159,14,171,9,341,0v-11,-122,3,-253,80,-296v23,-13,54,-9,90,6v-23,128,-34,302,-10,440v12,71,91,153,40,230","w":824},"I":{"d":"147,-657v49,72,67,160,58,290v-3,51,-5,88,-7,111r-24,228v-7,37,-23,61,-63,63v-136,-74,-34,-349,-87,-523v-18,-60,-32,-160,44,-170v24,-3,51,-3,79,1","w":224},"J":{"d":"798,-672v-59,78,-177,77,-268,123v15,100,64,203,72,309v6,79,-27,124,-72,168v-97,96,-412,144,-496,0v-15,-25,-17,-57,-11,-92v20,-46,104,-39,140,-9v-7,43,14,70,63,83v99,24,266,-33,275,-120v13,-120,-75,-207,-96,-302v-76,1,-172,35,-247,12v-27,-8,-53,-20,-76,-40v7,-7,9,-23,6,-48v-3,-25,2,-42,16,-53v228,99,449,-41,694,-31","w":814},"K":{"d":"158,-109v-15,62,28,130,-8,179v-30,-7,-56,-36,-80,-89v-24,-53,-52,-118,-52,-199v0,-160,-5,-365,55,-475r62,0v22,146,-24,251,-8,405v23,-7,55,-18,97,-33v84,-30,128,-44,232,-50v26,-2,61,15,88,0v36,-20,88,-22,99,23r-376,122v14,33,70,32,109,40v107,21,144,106,259,119v0,15,-15,28,0,39v13,-48,92,-12,86,23r-141,-8r-118,-17"},"L":{"d":"m70,-669v29,-5,57,12,75,19v50,141,28,372,18,538v151,-57,302,-44,470,-84v59,-14,116,0,133,46v-16,29,-72,32,-75,75v-252,-2,-408,98,-659,93v-34,-148,15,-357,10,-518v0,-14,-18,-83,-10,-103v9,-22,48,-32,38,-66","w":770},"M":{"d":"m577,-313v-50,88,-93,166,-185,208v-85,-47,-126,-128,-169,-227v-46,111,-52,284,-152,344v-19,11,-40,15,-64,12v-1,-205,86,-461,123,-624v54,-12,74,-97,147,-69v33,95,53,269,91,367v29,73,64,79,98,10v43,-89,55,-250,104,-328v29,-15,61,0,84,10v46,185,37,482,162,595v-39,33,-82,43,-120,9v-72,-65,-100,-188,-119,-307","w":829},"N":{"d":"776,-650v10,60,-21,238,-20,290v3,148,81,240,90,380v-101,52,-170,-51,-210,-120v-168,-63,-247,-185,-424,-239v-57,69,-32,194,-20,290v-22,24,-97,26,-130,10v-102,-145,-22,-420,60,-520v92,-30,137,42,190,80v98,69,202,93,284,179v54,-85,-1,-285,70,-360v46,-6,74,-7,110,10","w":861},"O":{"d":"151,-442v-65,-42,-81,-81,-41,-106v147,-90,427,-4,552,57v84,42,202,128,140,245v-82,153,-298,215,-530,215v-157,0,-296,-63,-245,-227v23,-72,72,-133,124,-184xm416,-452v-88,16,-156,49,-190,123v-19,41,-68,119,-24,161v56,53,184,35,273,24v86,-11,195,-29,243,-81v49,-53,27,-72,-35,-129v-73,-67,-76,-77,-175,-94v-39,-7,-69,-7,-92,-4","w":837},"P":{"d":"154,-334v-23,136,55,210,70,325v2,20,-3,43,-15,68v-65,-63,-131,-112,-143,-232v-9,-95,1,-173,-9,-271v-67,-56,-13,-179,42,-207v109,-57,340,-20,412,47v54,51,17,111,-33,146v-74,53,-232,91,-324,124xm154,-555v10,16,-6,125,0,145v104,-23,169,-60,262,-110v-29,-74,-196,-86,-262,-35","w":559},"Q":{"d":"774,-437v0,72,-37,116,-52,173v73,49,173,76,198,168v9,33,-6,61,-25,83v-66,-69,-192,-107,-285,-147v-115,53,-226,179,-406,164v-135,-11,-208,-116,-182,-277v21,-132,121,-247,303,-345v23,11,40,22,51,34v66,-45,167,-68,260,-40v74,22,138,91,138,187xm532,-212v-48,-16,-97,-68,-86,-135v6,-40,76,-60,105,-28v24,26,43,61,85,68v43,-51,60,-186,-17,-216v-180,-7,-325,58,-411,157v-42,49,-68,115,-48,206v20,91,147,75,224,40v54,-25,115,-57,148,-92","w":872},"R":{"d":"665,-554v-25,182,-190,240,-295,340v41,21,454,98,488,130v6,21,-20,44,-18,62v-193,28,-398,-29,-577,-49v-48,9,-26,95,-90,89r-43,-150v-17,-66,-22,-135,-19,-216v-22,-22,-37,27,-71,9v-36,-41,-44,-82,-12,-125v39,-52,109,-91,128,-161v113,-47,325,-77,438,-5v32,20,56,45,71,76xm227,-473v20,86,-10,174,18,250v109,-92,272,-151,322,-304v-106,-54,-286,-24,-340,54","w":888},"S":{"d":"459,-505v-90,-72,-263,-16,-298,67v-13,30,-16,71,2,90v97,101,335,105,425,213v19,87,-53,99,-109,125v-113,21,-252,29,-418,23r0,-74v64,-45,197,-50,287,-65v-85,-96,-301,-77,-333,-222v-18,-81,50,-206,102,-240v71,-46,185,-105,303,-97v61,4,102,52,90,116v-6,28,-23,49,-51,64","w":613},"T":{"d":"26,-563v0,-37,32,-51,52,-70v228,14,415,9,600,-7v45,-4,86,11,102,42v13,24,-27,9,-17,35v-47,-14,-201,30,-251,17v-12,164,-75,354,-44,537v-15,41,-53,59,-112,52v-54,-166,7,-386,17,-554v-91,-34,-229,28,-319,-6v-19,-7,-28,-24,-28,-46","w":806},"U":{"d":"740,-588v61,234,-38,452,-203,528v-182,84,-375,-31,-455,-150v-44,-66,-77,-158,-54,-267v20,-20,86,-8,122,-11v35,83,63,139,84,169v66,92,150,132,284,120v83,-54,114,-261,33,-344v7,-74,133,-83,189,-45","w":771},"V":{"d":"693,-722v-121,131,-138,386,-179,595v-32,46,-97,90,-118,148v-67,9,-88,-69,-119,-99v-84,-81,-207,-144,-254,-264v-19,-50,-15,-102,6,-152v43,-8,74,1,109,10v39,127,159,194,228,288v75,-167,83,-427,188,-565v66,-32,113,-13,139,39","w":715},"W":{"d":"925,-636v17,153,26,262,25,328v-2,201,-58,323,-173,347v-186,38,-281,-96,-381,-193r-35,-38v-74,47,-131,142,-259,138v-92,-78,-111,-301,-74,-453v17,-7,51,-10,102,-9v41,85,14,238,56,324v121,2,35,-207,148,-222v114,-15,141,132,203,194v51,51,112,117,202,126v81,8,88,-107,85,-184v-4,-106,-31,-214,-20,-324v3,-30,14,-54,29,-71v46,-10,63,25,92,37","w":973},"X":{"d":"57,113v52,-192,161,-319,303,-429v-80,-32,-145,-115,-230,-144v-29,-10,-57,-11,-83,-2v-43,-36,-46,-88,-11,-157v164,-25,317,35,460,178v129,-96,237,-187,324,-272v52,-4,82,10,115,31v-46,146,-202,206,-293,303v94,87,272,110,355,209v1,36,-51,0,-41,53v-159,-16,-271,-47,-377,-126v-195,74,-344,192,-386,418v-20,-5,-29,8,-42,11v-22,-10,-72,-64,-94,-73","w":997},"Y":{"d":"140,-622v13,94,4,210,89,232v106,28,213,-53,294,-72v0,-87,86,-152,160,-89v18,205,-88,339,-107,525v-11,109,-14,242,-142,233v-40,-204,49,-369,71,-553v-122,19,-323,121,-436,18v-62,-57,-77,-215,-27,-303v33,-9,65,-6,98,9","w":706},"Z":{"d":"43,-614v182,28,478,-14,607,72v-16,270,-358,235,-471,408v178,-21,436,-164,598,-27v-12,39,-71,80,-118,45v-221,-2,-384,141,-616,91v-50,-213,140,-259,281,-328v52,-26,126,-62,163,-98v-127,-40,-314,2,-453,-27v-22,-40,-24,-107,9,-136","w":801},"\\":{"d":"62,-720v-12,1,-45,-8,-37,13v3,57,13,88,46,115v108,226,285,414,386,647v16,-6,45,-4,52,-20v-47,-147,-144,-254,-219,-370v-97,-149,-112,-169,-219,-371v0,-4,-9,-10,-9,-14","w":550},"_":{"d":"612,-90v4,40,-28,61,-48,78v-200,-4,-300,-6,-301,-6v-73,-1,-159,35,-225,7v-21,-9,-28,-34,-21,-67v216,-29,414,-33,595,-12","w":640},"`":{"d":"110,-782v63,60,113,142,202,175v19,7,39,5,58,0v15,10,-6,44,20,46v-38,44,-96,64,-161,37v-88,-37,-160,-119,-203,-202v20,-38,34,-59,84,-56","w":430},"a":{"d":"162,-161v42,-38,90,-92,171,-93v-1,-33,-73,-159,-78,-191v-126,100,-71,377,-238,443v-33,-103,21,-202,50,-278r101,-258v15,-41,28,-83,40,-128v74,-25,66,94,130,53v45,118,79,252,158,337v59,10,131,4,148,59v-28,13,-39,31,-53,60v21,35,39,61,54,80v2,51,-33,61,-68,79v-44,-18,-135,-142,-179,-160v-68,-10,-179,58,-227,65","w":656},"b":{"d":"31,-421v0,-13,-16,-40,-15,-54v40,-37,86,-58,137,-61v-6,-48,-1,-88,15,-119v18,-37,44,-38,77,-3v5,19,2,50,-8,92v89,7,191,0,229,61v9,67,-41,96,-61,137v50,1,226,-11,287,0v49,9,88,22,117,46v6,116,-104,160,-152,221v-16,6,-59,26,-130,57v-121,53,-186,84,-328,88v13,-78,-20,-152,-30,-214v-45,9,-88,9,-130,0v-36,-84,61,-98,114,-129v-3,-44,9,-119,-7,-153v-25,3,-89,29,-115,31xm237,-452r0,92v54,-4,127,-56,145,-99v-40,-24,-104,-11,-145,7xm252,-208v15,37,-14,134,-15,160v223,-48,378,-122,465,-221v-140,-39,-290,-19,-450,61","w":840},"c":{"d":"550,-632v13,83,-80,57,-149,70v-99,19,-146,121,-220,170v-61,182,23,344,216,294v141,-37,221,-154,383,-165v-51,106,-303,266,-449,280v-170,16,-281,-65,-311,-203v-43,-197,86,-325,202,-416v107,-84,188,-96,328,-30","w":817},"d":{"d":"194,-520v-33,165,-29,290,17,434v-38,68,-74,78,-106,28v-45,-70,-72,-240,-47,-374v15,-78,17,-99,-20,-140v-15,-16,-20,-31,-17,-43v8,1,6,-2,8,-9v66,-54,112,-63,139,-26v84,-10,274,62,338,70v41,5,185,119,208,138v46,38,78,123,78,200v0,34,-25,71,-60,113v-186,120,-366,226,-642,225r0,-69v33,-6,66,-12,98,-19v159,-37,341,-85,450,-174v72,-59,43,-183,-22,-229v-104,-74,-258,-110,-422,-125","w":836},"e":{"d":"373,-313v10,216,242,212,463,217v40,1,72,6,94,12v13,68,-58,70,-119,79v-103,14,-283,0,-338,-40v-21,-5,-34,30,-60,20v-125,-49,-195,-137,-208,-268v-75,-12,-103,49,-179,40v-3,-30,-11,-61,0,-90v12,-30,134,-42,149,-69v-16,-68,-60,-157,-20,-229v280,-26,559,25,755,120v-163,63,-426,-23,-586,39v15,41,47,42,98,30v26,-7,42,-10,51,-10v121,0,220,13,298,40v-47,48,-338,82,-398,109","w":920},"f":{"d":"917,-562v-248,3,-453,28,-619,108v-2,44,4,100,-9,132v110,-32,208,-104,355,-78v33,6,68,21,105,40v-27,49,-100,69,-220,62v-142,60,-124,49,-240,134v-8,70,12,166,-62,176v-27,-18,-39,-65,-35,-141v-6,-34,-37,4,-52,-17v-22,-61,29,-80,52,-106v-7,-59,-4,-129,9,-211v-64,13,-131,30,-205,9v-11,-24,-20,-58,-8,-88v44,-18,165,-11,213,-27v-14,-50,51,-122,101,-99v17,9,27,27,31,55v89,5,130,-10,205,-20v240,-31,366,-8,379,71","w":867},"g":{"d":"389,-656v7,58,10,108,8,149v-37,9,-65,2,-84,-19v-108,33,-186,148,-164,296v6,44,26,82,55,113v59,35,147,56,224,25v55,-22,109,-56,160,-81v-21,-45,-29,-89,-86,-76v-49,11,-119,13,-165,-9v-23,-11,-44,-16,-61,-17r0,-93v49,-36,104,-61,172,-46v90,20,189,45,302,38v81,-5,68,17,144,54v-29,19,-42,30,-41,35v-15,-3,-59,-2,-73,9v0,10,16,17,10,31v-16,-9,-73,5,-93,0v0,61,-3,99,-8,112v-110,91,-262,181,-463,152v-174,-25,-248,-186,-199,-413v27,-126,119,-231,241,-266v40,-11,81,-9,121,6","w":845},"h":{"d":"800,46v-130,-34,-165,-158,-190,-300v-63,61,-246,42,-341,70v-5,37,45,83,40,120v-55,36,-103,33,-142,-10v-35,-39,-57,-90,-68,-150v-60,-14,-112,-69,-80,-150v16,-17,60,-10,80,-20v-1,-80,-1,-130,2,-149v12,-74,43,-107,118,-101v47,67,23,216,40,310v159,14,171,9,341,0v-11,-122,3,-253,80,-296v23,-13,54,-9,90,6v-23,128,-34,302,-10,440v12,71,91,153,40,230","w":824},"i":{"d":"147,-657v49,72,67,160,58,290v-3,51,-5,88,-7,111r-24,228v-7,37,-23,61,-63,63v-136,-74,-34,-349,-87,-523v-18,-60,-32,-160,44,-170v24,-3,51,-3,79,1","w":224},"j":{"d":"798,-672v-59,78,-177,77,-268,123v15,100,64,203,72,309v6,79,-27,124,-72,168v-97,96,-412,144,-496,0v-15,-25,-17,-57,-11,-92v20,-46,104,-39,140,-9v-7,43,14,70,63,83v99,24,266,-33,275,-120v13,-120,-75,-207,-96,-302v-76,1,-172,35,-247,12v-27,-8,-53,-20,-76,-40v7,-7,9,-23,6,-48v-3,-25,2,-42,16,-53v228,99,449,-41,694,-31","w":814},"k":{"d":"158,-109v-15,62,28,130,-8,179v-30,-7,-56,-36,-80,-89v-24,-53,-52,-118,-52,-199v0,-160,-5,-365,55,-475r62,0v22,146,-24,251,-8,405v23,-7,55,-18,97,-33v84,-30,128,-44,232,-50v26,-2,61,15,88,0v36,-20,88,-22,99,23r-376,122v14,33,70,32,109,40v107,21,144,106,259,119v0,15,-15,28,0,39v13,-48,92,-12,86,23r-141,-8r-118,-17"},"l":{"d":"m70,-669v29,-5,57,12,75,19v50,141,28,372,18,538v151,-57,302,-44,470,-84v59,-14,116,0,133,46v-16,29,-72,32,-75,75v-252,-2,-408,98,-659,93v-34,-148,15,-357,10,-518v0,-14,-18,-83,-10,-103v9,-22,48,-32,38,-66","w":770},"m":{"d":"m577,-313v-50,88,-93,166,-185,208v-85,-47,-126,-128,-169,-227v-46,111,-52,284,-152,344v-19,11,-40,15,-64,12v-1,-205,86,-461,123,-624v54,-12,74,-97,147,-69v33,95,53,269,91,367v29,73,64,79,98,10v43,-89,55,-250,104,-328v29,-15,61,0,84,10v46,185,37,482,162,595v-39,33,-82,43,-120,9v-72,-65,-100,-188,-119,-307","w":829},"n":{"d":"776,-650v10,60,-21,238,-20,290v3,148,81,240,90,380v-101,52,-170,-51,-210,-120v-168,-63,-247,-185,-424,-239v-57,69,-32,194,-20,290v-22,24,-97,26,-130,10v-102,-145,-22,-420,60,-520v92,-30,137,42,190,80v98,69,202,93,284,179v54,-85,-1,-285,70,-360v46,-6,74,-7,110,10","w":861},"o":{"d":"151,-442v-65,-42,-81,-81,-41,-106v147,-90,427,-4,552,57v84,42,202,128,140,245v-82,153,-298,215,-530,215v-157,0,-296,-63,-245,-227v23,-72,72,-133,124,-184xm416,-452v-88,16,-156,49,-190,123v-19,41,-68,119,-24,161v56,53,184,35,273,24v86,-11,195,-29,243,-81v49,-53,27,-72,-35,-129v-73,-67,-76,-77,-175,-94v-39,-7,-69,-7,-92,-4","w":837},"p":{"d":"154,-334v-23,136,55,210,70,325v2,20,-3,43,-15,68v-65,-63,-131,-112,-143,-232v-9,-95,1,-173,-9,-271v-67,-56,-13,-179,42,-207v109,-57,340,-20,412,47v54,51,17,111,-33,146v-74,53,-232,91,-324,124xm154,-555v10,16,-6,125,0,145v104,-23,169,-60,262,-110v-29,-74,-196,-86,-262,-35","w":559},"q":{"d":"774,-437v0,72,-37,116,-52,173v73,49,173,76,198,168v9,33,-6,61,-25,83v-66,-69,-192,-107,-285,-147v-115,53,-226,179,-406,164v-135,-11,-208,-116,-182,-277v21,-132,121,-247,303,-345v23,11,40,22,51,34v66,-45,167,-68,260,-40v74,22,138,91,138,187xm532,-212v-48,-16,-97,-68,-86,-135v6,-40,76,-60,105,-28v24,26,43,61,85,68v43,-51,60,-186,-17,-216v-180,-7,-325,58,-411,157v-42,49,-68,115,-48,206v20,91,147,75,224,40v54,-25,115,-57,148,-92","w":872},"r":{"d":"665,-554v-25,182,-190,240,-295,340v41,21,454,98,488,130v6,21,-20,44,-18,62v-193,28,-398,-29,-577,-49v-48,9,-26,95,-90,89r-43,-150v-17,-66,-22,-135,-19,-216v-22,-22,-37,27,-71,9v-36,-41,-44,-82,-12,-125v39,-52,109,-91,128,-161v113,-47,325,-77,438,-5v32,20,56,45,71,76xm227,-473v20,86,-10,174,18,250v109,-92,272,-151,322,-304v-106,-54,-286,-24,-340,54","w":888},"s":{"d":"459,-505v-90,-72,-263,-16,-298,67v-13,30,-16,71,2,90v97,101,335,105,425,213v19,87,-53,99,-109,125v-113,21,-252,29,-418,23r0,-74v64,-45,197,-50,287,-65v-85,-96,-301,-77,-333,-222v-18,-81,50,-206,102,-240v71,-46,185,-105,303,-97v61,4,102,52,90,116v-6,28,-23,49,-51,64","w":613},"t":{"d":"26,-563v0,-37,32,-51,52,-70v228,14,415,9,600,-7v45,-4,86,11,102,42v13,24,-27,9,-17,35v-47,-14,-201,30,-251,17v-12,164,-75,354,-44,537v-15,41,-53,59,-112,52v-54,-166,7,-386,17,-554v-91,-34,-229,28,-319,-6v-19,-7,-28,-24,-28,-46","w":806},"u":{"d":"740,-588v61,234,-38,452,-203,528v-182,84,-375,-31,-455,-150v-44,-66,-77,-158,-54,-267v20,-20,86,-8,122,-11v35,83,63,139,84,169v66,92,150,132,284,120v83,-54,114,-261,33,-344v7,-74,133,-83,189,-45","w":771},"v":{"d":"693,-722v-121,131,-138,386,-179,595v-32,46,-97,90,-118,148v-67,9,-88,-69,-119,-99v-84,-81,-207,-144,-254,-264v-19,-50,-15,-102,6,-152v43,-8,74,1,109,10v39,127,159,194,228,288v75,-167,83,-427,188,-565v66,-32,113,-13,139,39","w":715},"w":{"d":"925,-636v17,153,26,262,25,328v-2,201,-58,323,-173,347v-186,38,-281,-96,-381,-193r-35,-38v-74,47,-131,142,-259,138v-92,-78,-111,-301,-74,-453v17,-7,51,-10,102,-9v41,85,14,238,56,324v121,2,35,-207,148,-222v114,-15,141,132,203,194v51,51,112,117,202,126v81,8,88,-107,85,-184v-4,-106,-31,-214,-20,-324v3,-30,14,-54,29,-71v46,-10,63,25,92,37","w":973},"x":{"d":"57,113v52,-192,161,-319,303,-429v-80,-32,-145,-115,-230,-144v-29,-10,-57,-11,-83,-2v-43,-36,-46,-88,-11,-157v164,-25,317,35,460,178v129,-96,237,-187,324,-272v52,-4,82,10,115,31v-46,146,-202,206,-293,303v94,87,272,110,355,209v1,36,-51,0,-41,53v-159,-16,-271,-47,-377,-126v-195,74,-344,192,-386,418v-20,-5,-29,8,-42,11v-22,-10,-72,-64,-94,-73","w":997},"y":{"d":"140,-622v13,94,4,210,89,232v106,28,213,-53,294,-72v0,-87,86,-152,160,-89v18,205,-88,339,-107,525v-11,109,-14,242,-142,233v-40,-204,49,-369,71,-553v-122,19,-323,121,-436,18v-62,-57,-77,-215,-27,-303v33,-9,65,-6,98,9","w":706},"z":{"d":"43,-614v182,28,478,-14,607,72v-16,270,-358,235,-471,408v178,-21,436,-164,598,-27v-12,39,-71,80,-118,45v-221,-2,-384,141,-616,91v-50,-213,140,-259,281,-328v52,-26,126,-62,163,-98v-127,-40,-314,2,-453,-27v-22,-40,-24,-107,9,-136","w":801},"\u00a0":{"w":383}}}); 9 | -------------------------------------------------------------------------------- /lib/pencil/public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #000; 3 | color: #fff; 4 | margin: 0px; 5 | font-family: Verdana, Arial, Tahoma, sans-serif; 6 | min-width: 1200px; 7 | } 8 | 9 | #header { 10 | padding: 10px; 11 | border-bottom: 3px #222 solid; 12 | height: 45px; 13 | background-color: #111; 14 | } 15 | 16 | #header h1{ 17 | padding: 0px; 18 | margin: 0px; 19 | font-weight: bold; 20 | } 21 | 22 | #header h4{ 23 | margin: 0px; 24 | padding: 0px; 25 | color: #ccc; 26 | } 27 | 28 | div#nav { 29 | width: 15%; 30 | padding: 10px; 31 | float: left; 32 | color: green; 33 | border-left: 1px #444 solid; 34 | border-right: 1px #444 solid; 35 | border-bottom: 1px #444 solid; 36 | background-color: #222; 37 | font-size: 12px; 38 | } 39 | 40 | div#nav h3{ 41 | color: #d0cfcf; 42 | font-size: 12px; 43 | } 44 | 45 | div#nav ul{ 46 | list-style-type: square; 47 | padding-left: 15px; 48 | } 49 | 50 | div#main ul{ 51 | list-style-type: square; 52 | padding-left: 25px; 53 | } 54 | 55 | div#nav ul li a, 56 | div#main ul li a { 57 | color: #99ccff; 58 | } 59 | 60 | div.nav-box { 61 | margin-top: 10px; 62 | background-color: #111; 63 | font-size: 10px; 64 | } 65 | 66 | div.nav-box-inner { 67 | padding: 3px; 68 | color: #fff; 69 | } 70 | 71 | div.nav-box-inner label{ 72 | clear: left; 73 | width: 70px; 74 | padding-right: 4px; 75 | float: left; 76 | display: inline; 77 | text-align: right; 78 | height: 16px; 79 | margin-top: 4px; 80 | } 81 | 82 | div.nav-box-inner input{ 83 | float: left; 84 | display: inline-block; 85 | height: 16px; 86 | margin-top: 4px; 87 | color: #fff; 88 | } 89 | 90 | div.nav-box-inner div.button-holder input { 91 | float: none; 92 | height: 20px; 93 | font-size: 9px; 94 | } 95 | 96 | 97 | div.nav-box-inner div.button-holder { 98 | text-align: center; 99 | font-size: 10px; 100 | clear: both; 101 | padding-top: 5px; 102 | } 103 | 104 | div.nav-box-inner a.inline-link { 105 | float: left; 106 | position: relative; 107 | top: 4px; 108 | left: 4px; 109 | color: #f0f0f0; 110 | } 111 | 112 | div.nav-box h3 { 113 | margin: 0px; 114 | padding: 2px; 115 | font-size: 11px; 116 | } 117 | 118 | div#timeslice_container{ 119 | position: relative; 120 | } 121 | 122 | div#timeslice_container a{ 123 | position: absolute; 124 | top: 0px; 125 | color: #fff; 126 | right: 0px; 127 | text-align: right; 128 | } 129 | 130 | div.group_container { 131 | background-color: #111; 132 | border: 1px #333 solid; 133 | padding: 2px; 134 | font-size: 13px; 135 | } 136 | 137 | div.group_container.first { 138 | border-bottom: none; 139 | } 140 | div.group_container.last { 141 | 142 | } 143 | 144 | div.group_container a { 145 | color: #99ccff; 146 | font-size: 13px; 147 | } 148 | 149 | div.group_container h4 { 150 | display: inline-block; 151 | margin: 0px; 152 | padding: 0px; 153 | font-size: 16px; 154 | } 155 | 156 | div.path_container { 157 | position: relative; 158 | } 159 | 160 | div.path_container span.zoom { 161 | position: absolute; 162 | top: 7px; 163 | right:0px; 164 | } 165 | 166 | div.path_container span.zoom a { 167 | color: #99ccff; 168 | } 169 | 170 | div.group_container_small { 171 | margin-top: 15px; 172 | background-color: #333; 173 | border: 1px #555 solid; 174 | padding: 2px; 175 | font-size: 11px; 176 | } 177 | 178 | div.group_container_small a { 179 | color: #99ccff; 180 | font-size: 11px; 181 | } 182 | 183 | div.group_container_small h4 { 184 | display: inline-block; 185 | margin: 0px; 186 | padding: 0px; 187 | font-size: 14px; 188 | } 189 | 190 | div.graph_container { 191 | position: relative; 192 | border: 1px #333 dotted; 193 | margin-top: 10px; 194 | } 195 | 196 | div.graph_container h3{ 197 | border-bottom: 1px #666666 solid; 198 | background-color: #222; 199 | margin: 0px; 200 | padding: 2px; 201 | } 202 | 203 | div.graph_container h3 span.tools{ 204 | position: absolute; 205 | top:5px; 206 | right:5px; 207 | font-size: 11px; 208 | color: #fff; 209 | } 210 | 211 | div.graph_container h3 span.tools a{ 212 | color: #99ccff; 213 | } 214 | 215 | div.graph_container div.graph{ 216 | text-align: center; 217 | padding-bottom: 8px; 218 | } 219 | 220 | h2 { 221 | color: #ADFF2F; 222 | } 223 | 224 | h3, 225 | h3 a{ 226 | color: #32cd32; 227 | } 228 | 229 | .graphtitle { 230 | text-shadow: 1px 1px 1px #333333; 231 | } 232 | 233 | .dashlinks { 234 | color: green; 235 | font-size: small; 236 | } 237 | 238 | a { 239 | color: #15317E; 240 | } 241 | 242 | a:visited { 243 | color: #15317E; 244 | } 245 | 246 | .graphsection { 247 | border: dotted 2px #333333; 248 | width: 500; 249 | margin-left: 5px; 250 | margin-top: 10px; 251 | } 252 | 253 | 254 | div#main { 255 | margin-left: 18%; 256 | padding: 10px; 257 | color: #4CBB17; 258 | margin-bottom: 30px; 259 | } 260 | 261 | div#footer { 262 | width: 100%; 263 | background-color: #222; 264 | position: fixed; 265 | bottom: 0px; 266 | text-align: center; 267 | border-top: 1px #333 solid; 268 | margin-top: 15px; 269 | font-size: 11px; 270 | padding: 5px; 271 | color: #fff; 272 | } 273 | 274 | div#footer a{ 275 | color: #ccc; 276 | font-weight: bold; 277 | } 278 | 279 | table { 280 | color: green; 281 | } 282 | 283 | input { 284 | border: solid 1px #15317E; 285 | color: green; 286 | background-color: #1f1f1f; 287 | font-size: small; 288 | } 289 | 290 | .invisible { 291 | display: none; 292 | } 293 | 294 | select { 295 | border: solid 1px #15317E; 296 | color: green; 297 | background-color: #1f1f1f; 298 | } 299 | 300 | .select2 { 301 | color: #4CBB17; 302 | font-size: large; 303 | } 304 | 305 | .error { 306 | color: red; 307 | } 308 | -------------------------------------------------------------------------------- /lib/pencil/rubyfixes.rb: -------------------------------------------------------------------------------- 1 | class YAML::Omap 2 | def keys 3 | self.map {|k,v| k} 4 | end 5 | def values 6 | self.map {|k,v| v} 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/pencil/version.rb: -------------------------------------------------------------------------------- 1 | module Pencil 2 | VERSION = "0.3.1" 3 | end 4 | -------------------------------------------------------------------------------- /lib/pencil/views/cluster.erb: -------------------------------------------------------------------------------- 1 |

List of dashboards for <%= cluster_selector %>

2 | 3 | <% hosts = Host.all.select { |h| h.cluster == @cluster } 4 | boards = @dashboards.select { |d| d.clusters.member?(@cluster) } 5 | seen_hosts = Set.new %> 6 | 7 | <% boards.sort.each do |b| %> 8 |

"><%= b.name %> 9 | <% dash_hosts = b.get_all_hosts(@cluster)[0] 10 | seen_hosts += dash_hosts %> 11 | <%= hosts_selector(dash_hosts) %> 12 |

13 |
    14 | <%= b.graphs.collect do |g| 15 | href = append_query_string("/dash/#{@cluster}/#{b.name}/#{g}") 16 | "
  • #{g}
  • " 17 | end %> 18 |
    19 |
20 | <% end %> 21 | 22 | <% if (hosts.to_set - seen_hosts).size > 0 %> 23 |

Other Hosts (not associated with a dashboard)

24 |
    25 | <%= (hosts.to_set - seen_hosts).sort.collect do |h| 26 | href = append_query_string("/host/#{@cluster}/#{h}") 27 | "
  • #{h}
  • " 28 | end %> 29 | <% end %> 30 |
31 | -------------------------------------------------------------------------------- /lib/pencil/views/dash-cluster-zoom.erb: -------------------------------------------------------------------------------- 1 | <%= header(graph_uplink) %> 2 | <% hosts = @dash.get_valid_hosts(@zoom, @cluster)[0].sort 3 | width = @zoom.width(merge_opts) %> 4 | 5 | <%= cluster_switcher(@dash.clusters) %> 6 | <%= graph_switcher %> 7 | <%= shortcuts(hosts.collect { |h| "#{h}" }.join(" ")) %> 8 | 9 |
10 |

<%= @zoom.name %> / <%= @cluster %> :: summary 11 |

12 |
13 | 14 |
15 |
16 | 17 | <% hosts.each do |host| %> 18 |
19 | 20 | <% image_url, zoom_url = cluster_zoom_graph(@zoom, @cluster, host, "#{@zoom.name} / #{@cluster} / #{host}") %> 21 |

<%= @zoom.name %> / <%= @cluster %> / <%= host %> 22 | (host) 23 |

24 |
25 | 26 |
27 |
28 | <% end %> 29 | -------------------------------------------------------------------------------- /lib/pencil/views/dash-cluster.erb: -------------------------------------------------------------------------------- 1 | <%= header(dash_uplink) %> 2 | <%= cluster_switcher(@dash.clusters) %> 3 | <%= dash_switcher %> 4 | <%= shortcuts(@dash.graphs.collect { |g| "#{g.name}" }.join(" ")) %> 5 | 6 | <% @dash.graphs.each do |g| %> 7 |
8 | 9 | <% image_url, zoom_url = cluster_graph(g, @cluster, "#{g.name} / #{@cluster}") %> 10 |

<%= g.name %> 11 | (zoom) 12 |

13 |
14 | 15 |
16 |
17 | <% end %> 18 | -------------------------------------------------------------------------------- /lib/pencil/views/dash-global-zoom.erb: -------------------------------------------------------------------------------- 1 | <%= header(graph_uplink) %> 2 | <% hosts, clusters = @dash.get_valid_hosts(@zoom) 3 | width = @zoom.width(merge_opts) %> 4 | 5 | <%= cluster_switcher(@dash.clusters) %> 6 | <%= graph_switcher %> 7 | <%= shortcuts(clusters.sort.collect { |h| "#{h}" }.join(" ")) %> 8 | 9 |
10 | <% graph_name = "#{@zoom.name} overview" %> 11 |

<%= graph_name %>

12 |
13 | 14 |
15 |
16 | 17 | <% clusters.sort.each do |cluster| %> 18 |
19 | 20 | <% graph_name = "#{@dash.name} / #{@zoom.name} / #{cluster}" %> 21 | <% image_url, zoom_url = cluster_graph(@zoom, cluster, graph_name) %> 22 |

<%= graph_name %> 23 | (zoom) 24 |

25 |
26 | 27 |
28 |
29 | <% end %> 30 | -------------------------------------------------------------------------------- /lib/pencil/views/dash-global.erb: -------------------------------------------------------------------------------- 1 | <%= header(dash_uplink) %> 2 | <% clusters = @dash.clusters %> 3 | <%= cluster_switcher(clusters) %> 4 | <%= dash_switcher %> 5 | <%= shortcuts(@dash.graphs.collect { |g| "#{g.name}" }.join(" ")) %> 6 | 7 | <% @dash.graphs.each do |g| %> 8 |
9 | 10 |

<%= g.name %> 11 | 12 | <%= suggest_cluster_links(clusters, g) %> 13 | 14 |

15 |
16 | <% zoom_url = cluster_graph_link(@dash, g, "global") %> 17 | 18 |
19 |
20 | <% end %> 21 | -------------------------------------------------------------------------------- /lib/pencil/views/global.erb: -------------------------------------------------------------------------------- 1 |

List of dashboards for <%= cluster_selector %>

2 | 3 | <% hosts = Host.all 4 | boards = @dashboards 5 | seen_hosts = Set.new %> 6 | 7 | <% boards.sort.each do |b| %> 8 |

"><%= b.name %> 9 | <% dash_hosts = b.get_all_hosts()[0] 10 | seen_hosts += dash_hosts %> 11 | <%= hosts_selector(dash_hosts, true) %> 12 |

13 |
    14 | <%= b.graphs.collect do |g| 15 | href = append_query_string("/dash/#{@cluster}/#{b.name}/#{g}") 16 | "
  • #{g}
  • " 17 | end %> 18 |
    19 |
20 | <% end %> 21 | 22 |

clusters :: hosts

23 |
    24 | <% map = Hash.new([]) 25 | hosts.each { |h| map[h.cluster] = map[h.cluster] + [h] } 26 | map.sort.each do |k, v| %> 27 |
28 |

<%= k %>

29 |
    30 | <%= v.sort.collect do |h| 31 | href = append_query_string("/host/#{h.cluster}/#{h}") 32 | "
  • #{h}
  • " 33 | end %> 34 | <% end %> 35 |
36 | -------------------------------------------------------------------------------- /lib/pencil/views/host.erb: -------------------------------------------------------------------------------- 1 | <%= header(host_uplink) %> 2 | <%= shortcuts(@host.graphs.collect { |g| "#{g.name}" }.join(" ")) %> 3 | 4 | <% @host.graphs.each do |g| %> 5 |
6 | 7 |

<%= g.name %> 8 | <%= suggest_dashboards_links(@host, g) %> 9 |

10 |
11 | merge_opts) %>"> 12 |
13 |
14 | <% end %> 15 | -------------------------------------------------------------------------------- /lib/pencil/views/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= css_url %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | <%= @title %> 14 | 18 | <%= refresh unless @no_graphs %> 19 | 20 | 21 |
22 | 26 | 27 |
28 |
29 | 59 | <% end %> 60 |
61 |
62 |
63 | <%= yield %> 64 |
65 |
66 |
67 |
68 |
69 | 72 |
73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /lib/pencil/views/partials/cluster_selector.erb: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /lib/pencil/views/partials/cluster_switcher.erb: -------------------------------------------------------------------------------- 1 |
2 |

clusters:

3 | <%= @clusters.sort.collect do |c| 4 | sub = append_query_string(request.path.sub(@cluster, c)) 5 | @cluster == c ? "#{c}" : "#{c}" 6 | end.join(" ") %> 7 | <%= if @cluster == "global" 8 | " (global)" 9 | else 10 | # this sub works because the cluster should be first 11 | " (global)" 12 | end %> 13 |
14 | -------------------------------------------------------------------------------- /lib/pencil/views/partials/dash_switcher.erb: -------------------------------------------------------------------------------- 1 |
2 |

dashboards:

3 | 4 | <%= @dashboards.select do |d| 5 | @cluster == "global" || d.clusters.member?(@cluster) 6 | end.sort.collect do |d| 7 | # this works unless you have a cluster with the same name as a dashboard 8 | sub = append_query_string(request.path.sub(@dash.name, d.name)) 9 | @dash == d ? "#{d}" : "#{d}" 10 | end.join(" ") %> 11 | 12 |
13 | -------------------------------------------------------------------------------- /lib/pencil/views/partials/graph_switcher.erb: -------------------------------------------------------------------------------- 1 |
2 |

graphs:

3 | 4 | <%= @dash.graphs.map { |g| g.name }.collect do |g| 5 | reversed = request.path.split("/").reverse 6 | i = reversed.index(@zoom.name) 7 | reversed[i] = g 8 | sub = append_query_string(reversed.reverse.join('/')) 9 | @zoom.name == g ? "#{g}" : "#{g}" 10 | end.join(" ") %> 11 | 12 |
13 | -------------------------------------------------------------------------------- /lib/pencil/views/partials/hosts_selector.erb: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /lib/pencil/views/partials/input_boxes.erb: -------------------------------------------------------------------------------- 1 | 37 | -------------------------------------------------------------------------------- /lib/pencil/views/partials/shortcuts.erb: -------------------------------------------------------------------------------- 1 |
2 |

shortcuts:

3 | <%= @str %> 4 |
5 | 6 | -------------------------------------------------------------------------------- /pencil.gemspec: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | require File.expand_path('../lib/pencil/version', __FILE__) 3 | 4 | Gem::Specification.new do |spec| 5 | files = [] 6 | dirs = %w(lib docs examples) 7 | dirs.each do |dir| 8 | files += Dir["#{dir}/**/*"] 9 | end 10 | 11 | spec.name = "pencil" 12 | spec.version = Pencil::VERSION 13 | spec.summary = "pencil -- Graphite dashboard system" 14 | spec.description = "Graphite dashboard frontend" 15 | spec.license = "Mozilla Public License (1.1)" 16 | 17 | spec.add_dependency("map") 18 | spec.add_dependency("rack") 19 | spec.add_dependency("sinatra") 20 | spec.add_dependency("json") 21 | spec.add_dependency("chronic") 22 | spec.add_dependency("chronic_duration") 23 | 24 | spec.files = files 25 | spec.bindir = "bin" 26 | spec.executables << "pencil" 27 | 28 | spec.authors = ["Pete Fritchman", "Wesley Dawson"] 29 | spec.email = ["petef@databits.net", "wdawson@mozilla.com"] 30 | spec.homepage = "https://github.com/fetep/pencil" 31 | end 32 | --------------------------------------------------------------------------------