├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── dialyzer.ignore-warnings ├── include └── riak_control.hrl ├── plugins ├── rebar_js_concatenator_plugin.erl ├── rebar_js_handlebars_plugin.erl └── rebar_js_stylus_plugin.erl ├── priv ├── admin │ ├── css │ │ ├── cluster.styl │ │ ├── compiled │ │ │ └── style.css │ │ ├── fonts.styl │ │ ├── general.styl │ │ ├── nodes.styl │ │ ├── reset.styl │ │ ├── reusable.styl │ │ ├── ring.styl │ │ ├── snapshot.styl │ │ └── style.styl │ ├── favicon.ico │ ├── fonts │ │ ├── noticiatext-italic-webfont.eot │ │ ├── noticiatext-italic-webfont.svg │ │ ├── noticiatext-italic-webfont.ttf │ │ ├── noticiatext-italic-webfont.woff │ │ ├── noticiatext-regular-webfont.eot │ │ ├── noticiatext-regular-webfont.svg │ │ ├── noticiatext-regular-webfont.ttf │ │ ├── noticiatext-regular-webfont.woff │ │ ├── titilliumtext25l002-webfont.eot │ │ ├── titilliumtext25l002-webfont.svg │ │ ├── titilliumtext25l002-webfont.ttf │ │ ├── titilliumtext25l002-webfont.woff │ │ ├── titilliumtext25l005-webfont.eot │ │ ├── titilliumtext25l005-webfont.svg │ │ ├── titilliumtext25l005-webfont.ttf │ │ └── titilliumtext25l005-webfont.woff │ ├── images │ │ ├── action-button-sprite.png │ │ ├── actions-toggle-slider.png │ │ ├── actions-toggle-text.png │ │ ├── add-phase.png │ │ ├── area-pointer.png │ │ ├── basho-logo.png │ │ ├── blue-rect.png │ │ ├── checkbox-sprite.png │ │ ├── close-error.png │ │ ├── cut-bg.png │ │ ├── dropdown-bg.png │ │ ├── dropdown-left.png │ │ ├── dropdown-right.png │ │ ├── expand.gif │ │ ├── field-bg.png │ │ ├── fieldcap-left.png │ │ ├── fieldcap-right.png │ │ ├── gui-bg.jpg │ │ ├── header-bg.png │ │ ├── healthy-cluster.png │ │ ├── hide-show-toggle.png │ │ ├── leavecluster-sprite.png │ │ ├── leaving-slider.png │ │ ├── light-sprite.png │ │ ├── markdown-sprite.png │ │ ├── mem-bar-overlay.png │ │ ├── membar-bg.png │ │ ├── membar-fg.png │ │ ├── nav-icons-small.png │ │ ├── nav-icons.png │ │ ├── node-bg.png │ │ ├── orange-arrow.png │ │ ├── pct-arrows-sprite.png │ │ ├── pointbutton-right-sprite.png │ │ ├── pointbutton-sprite.png │ │ ├── pointer.png │ │ ├── powerbutton-sprite.png │ │ ├── radio-sprite.png │ │ ├── rectbutton-sprite.png │ │ ├── resizer-bg.png │ │ ├── riak-control-logo-small.png │ │ ├── riak-control-logo.png │ │ ├── riak-transparent-small.png │ │ ├── right-angle-arrow.png │ │ ├── ring-indicator.png │ │ ├── slider-groove.png │ │ ├── slider-handle.png │ │ ├── spinner.gif │ │ ├── stagebutton-sprite.png │ │ ├── tooltip-mark.png │ │ └── unhealthy-cluster.png │ └── js │ │ ├── app.js │ │ ├── cluster.js │ │ ├── core.js │ │ ├── generated │ │ └── .gitkeep │ │ ├── legacy │ │ └── gui.js │ │ ├── nodes.js │ │ ├── ring.js │ │ ├── router.js │ │ ├── shared.js │ │ ├── snapshot.js │ │ ├── templates │ │ ├── all_unavailable_chart.hbs │ │ ├── application.hbs │ │ ├── cluster.hbs │ │ ├── current_cluster_item.hbs │ │ ├── current_nodes_item.hbs │ │ ├── degenerate_preflist_chart.hbs │ │ ├── loading.hbs │ │ ├── nodes.hbs │ │ ├── pagination_item.hbs │ │ ├── partition.hbs │ │ ├── quorum_unavailable_chart.hbs │ │ ├── ring.hbs │ │ ├── snapshot.hbs │ │ └── staged_cluster_item.hbs │ │ └── vendor │ │ ├── d3.v3.min.js │ │ ├── ember-1.0.0.js │ │ ├── ember-data-1.0.0-beta.1.js │ │ ├── handlebars-1.0.0.js │ │ ├── jquery-1.10.2.js │ │ ├── jquery-1.10.2.min.js │ │ ├── jquery-ui-1.8.16.custom.min.js │ │ └── minispade.js └── riak_control.schema ├── rebar ├── rebar.config ├── src ├── riak_control.app.src ├── riak_control.erl ├── riak_control_app.erl ├── riak_control_ring.erl ├── riak_control_routes.erl ├── riak_control_security.erl ├── riak_control_session.erl ├── riak_control_sup.erl ├── riak_control_wm_cluster.erl ├── riak_control_wm_gui.erl ├── riak_control_wm_nodes.erl └── riak_control_wm_partitions.erl ├── templates └── index.dtl ├── test ├── cert.pem ├── javascripts │ ├── admin │ ├── data │ │ └── mock_responses.js │ ├── index.html │ ├── pages_test.js │ ├── runner.js │ ├── support │ │ ├── jquery.mockjax.js │ │ ├── qunit-1.12.0.css │ │ └── qunit-1.12.0.js │ └── test_helper.js ├── key.pem └── riak_control_schema_test.erl └── tools.mk /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | ebin/* 3 | .eunit/* 4 | doc/* 5 | deps/* 6 | erl_crash.dump 7 | EUnit-SASL.log 8 | priv/admin/js/generated/templates.js 9 | priv/admin/js/generated/vendor.js 10 | tags 11 | erln8.config 12 | .local_dialyzer_plt 13 | dialyzer_unhandled_warnings 14 | dialyzer_warnings 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | notifications: 3 | webhooks: http://basho-engbot.herokuapp.com/travis?key=062744d781a235c60843701b05260c77d4a082b1 4 | email: eng@basho.com 5 | otp_release: 6 | - R15B01 7 | - R15B 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: rel deps test 2 | 3 | all: deps compile 4 | 5 | compile: deps 6 | @./rebar compile 7 | 8 | app: 9 | @./rebar compile skip_deps=true 10 | 11 | deps: 12 | @./rebar get-deps 13 | 14 | clean: 15 | @./rebar clean 16 | 17 | distclean: clean 18 | @./rebar delete-deps 19 | 20 | test: all test_erlang test_javascript 21 | 22 | test_erlang: all 23 | @./rebar skip_deps=true eunit 24 | 25 | define MISSING_PHANTOM_MESSAGE 26 | PhantomJS is missing to run the javascript tests, to install on your OS do the following 27 | 28 | - MacOS via homebrew run `brew install phantomjs` 29 | - Ubuntu `apt-get install phantomjs` 30 | - Other linux distros should check http://phantomjs.org/download.html to download a binary build 31 | 32 | or visit http://phantomjs.org/ for more information. 33 | endef 34 | export MISSING_PHANTOM_MESSAGE 35 | 36 | test_javascript: all 37 | ifeq ($(strip $(shell command -v phantomjs > /dev/null 2>&1 || echo 1)),) 38 | @echo "==> riak_control (qunit)" 39 | @phantomjs test/javascripts/runner.js test/javascripts/index.html 40 | else 41 | @echo "$$MISSING_PHANTOM_MESSAGE" 42 | endif 43 | 44 | include tools.mk 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Riak Control 2 | 3 | Riak Control is a OTP application included in the Riak database which 4 | provides a user interface for cluster planning and visibility. As Riak 5 | Control comes bundled with Riak, you do not need to install this 6 | application separately. 7 | 8 | ## Enabling Riak Control 9 | 10 | For more information on enabling Riak Control, see the 11 | [docs](http://docs.basho.com/riak/latest/ops/advanced/riak-control/). 12 | -------------------------------------------------------------------------------- /dialyzer.ignore-warnings: -------------------------------------------------------------------------------- 1 | Unknown types: 2 | webmachine_dispatcher:matchterm/0 3 | wrq:reqdata/0 4 | -------------------------------------------------------------------------------- /include/riak_control.hrl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2011 Basho Technologies, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | -type version() :: integer(). 22 | -type index() :: integer(). 23 | -type status() :: valid | invalid | down | leaving | incompatible | transitioning. 24 | -type home() :: primary | fallback | undefined. 25 | -type service() :: {atom(), home()}. 26 | -type services() :: [service()]. 27 | -type owner() :: atom(). 28 | -type vnode() :: {{atom(),atom()},atom()}. 29 | -type handoff() :: {atom(),integer(),atom()}. 30 | -type online() :: boolean(). 31 | -type ring() :: riak_core_ring:riak_core_ring(). 32 | -type handoffs() :: [handoff()]. 33 | -type vnodes() :: [vnode()]. 34 | -type plan() :: [] | legacy | ring_not_ready | unavailable. 35 | -type single_n_val() :: pos_integer(). 36 | -type n_vals() :: [pos_integer()]. 37 | 38 | -type stage_error() :: nodedown 39 | | already_leaving 40 | | not_member 41 | | only_member 42 | | is_claimant 43 | | invalid_replacement 44 | | already_replacement 45 | | not_reachable 46 | | not_single_node 47 | | self_join. 48 | 49 | -type action() :: leave 50 | | remove 51 | | {replace, node()} 52 | | {force_replace, node()}. 53 | 54 | -type claim_percentage() :: number(). 55 | 56 | -type change() :: {node(), action()}. 57 | 58 | -record(partition_info, 59 | { index :: index(), 60 | partition :: integer(), 61 | owner :: owner(), 62 | vnodes :: services(), 63 | handoffs :: handoffs() }). 64 | 65 | -define(PARTITION_INFO, #partition_info). 66 | -type partition() :: ?PARTITION_INFO{}. 67 | -type partitions() :: [partition()]. 68 | 69 | %% Riak 1.3 70 | -record(member_info, 71 | { node :: atom(), 72 | status :: status(), 73 | reachable :: boolean(), 74 | vnodes :: vnodes(), 75 | handoffs :: handoffs(), 76 | ring_pct :: float(), 77 | pending_pct :: float(), 78 | mem_total :: integer(), 79 | mem_used :: integer(), 80 | mem_erlang :: integer() }). 81 | 82 | %% Riak 1.4.1+ 83 | -record(member_info_v2, 84 | { node :: atom(), 85 | status :: status(), 86 | reachable :: boolean(), 87 | vnodes :: vnodes(), 88 | handoffs :: handoffs(), 89 | ring_pct :: float(), 90 | pending_pct :: float(), 91 | mem_total :: integer(), 92 | mem_used :: integer(), 93 | mem_erlang :: integer(), 94 | action :: action(), 95 | replacement :: node() }). 96 | 97 | -define(MEMBER_INFO, #member_info_v2). 98 | -type member() :: ?MEMBER_INFO{}. 99 | -type members() :: [member()]. 100 | 101 | %% These two should always match, in terms of webmachine dispatcher 102 | %% logic, and ADMIN_BASE_PATH should always end with a / 103 | -define(ADMIN_BASE_PATH, "/admin/"). 104 | -define(ADMIN_BASE_ROUTE, ["admin"]). 105 | 106 | %% Value for WWW-Authenticate header 107 | -define(ADMIN_AUTH_HEAD, "Basic realm=riak"). 108 | 109 | %% Names of HTTP header fields 110 | -define(HEAD_CTYPE, "Content-Type"). 111 | -------------------------------------------------------------------------------- /plugins/rebar_js_concatenator_plugin.erl: -------------------------------------------------------------------------------- 1 | %% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*- 2 | %% ex: ts=4 sw=4 et 3 | %% ------------------------------------------------------------------- 4 | %% 5 | %% rebar: Erlang Build Tools 6 | %% 7 | %% Copyright (c) 2012 Christopher Meiklejohn (christopher.meiklejohn@gmail.com) 8 | %% 9 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 10 | %% of this software and associated documentation files (the "Software"), to deal 11 | %% in the Software without restriction, including without limitation the rights 12 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | %% copies of the Software, and to permit persons to whom the Software is 14 | %% furnished to do so, subject to the following conditions: 15 | %% 16 | %% The above copyright notice and this permission notice shall be included in 17 | %% all copies or substantial portions of the Software. 18 | %% 19 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | %% THE SOFTWARE. 26 | %% ------------------------------------------------------------------- 27 | 28 | %% The rebar_js_concatenator_plugin module is a plugin for rebar that concatenates 29 | %% javascript files. 30 | %% 31 | %% Configuration options should be placed in rebar.config under 32 | %% 'js_concatenator'. Available options include: 33 | %% 34 | %% doc_root: where to find javascript files to concatenate 35 | %% "priv/assets/javascripts" by default 36 | %% 37 | %% out_dir: where to put concatenated javascript files 38 | %% "priv/assets/javascripts" by default 39 | %% 40 | %% concatenations: list of tuples describing each transformation. 41 | %% empty list by default 42 | %% 43 | %% The default settings are the equivalent of: 44 | %% {js_concatenator, [ 45 | %% {out_dir, "priv/assets/javascripts"}, 46 | %% {doc_root, "priv/assets/javascripts"}, 47 | %% {concatenations, []} 48 | %% ]}. 49 | %% 50 | %% An example of compiling a series of javascript files: 51 | %% 52 | %% {js_concatenator, [ 53 | %% {out_dir, "priv/assets/javascripts"}, 54 | %% {doc_root, "priv/assets/javascripts"}, 55 | %% {concatenations, [ 56 | %% {"vendor.js", ["ember.js", "jquery.js"], []}, 57 | %% {"application.js", ["models.js", "controllers.js"], []} 58 | %% ]} 59 | %% ]}. 60 | %% 61 | 62 | -module(rebar_js_concatenator_plugin). 63 | 64 | -ifdef(TEST). 65 | -include_lib("eunit/include/eunit.hrl"). 66 | -endif. 67 | 68 | -export([compile/2, 69 | clean/2]). 70 | 71 | -export([concatenate/1, 72 | concatenate_files/1]). 73 | 74 | %% =================================================================== 75 | %% Public API 76 | %% =================================================================== 77 | 78 | compile(Config, _AppFile) -> 79 | Options = options(Config), 80 | Concatenations = option(concatenations, Options), 81 | OutDir = option(out_dir, Options), 82 | DocRoot = option(doc_root, Options), 83 | Targets = [{normalize_path(Destination, OutDir), 84 | normalize_paths(Sources, DocRoot), 85 | ConcatOptions} || {Destination, Sources, ConcatOptions} <- Concatenations], 86 | build_each(Targets). 87 | 88 | clean(Config, _AppFile) -> 89 | Options = options(Config), 90 | Concatenations = option(concatenations, Options), 91 | OutDir = option(out_dir, Options), 92 | Targets = [normalize_path(Destination, OutDir) || 93 | {Destination, _Sources, _ConcatOptions} <- Concatenations], 94 | delete_each(Targets). 95 | 96 | %% @spec concatenate(list()) -> binary() 97 | %% @doc Given a list of sources, concatenate and return. 98 | concatenate(Sources) -> 99 | ListSources = [case is_binary(Source) of true -> 100 | binary_to_list(Source); false -> Source end || Source <- Sources], 101 | list_to_binary(lists:flatten(ListSources)). 102 | 103 | %% @spec concatenate_files(list()) -> list() 104 | %% @doc Given a list of source files, concatenate and return. 105 | concatenate_files(Sources) -> 106 | concatenate([read(Source) || Source <- Sources]). 107 | 108 | %% =================================================================== 109 | %% Internal functions 110 | %% =================================================================== 111 | 112 | options(Config) -> 113 | rebar_config:get_local(Config, js_concatenator, []). 114 | 115 | option(Option, Options) -> 116 | proplists:get_value(Option, Options, default(Option)). 117 | 118 | default(doc_root) -> "priv/assets/javascripts"; 119 | default(out_dir) -> "priv/assets/javascripts"; 120 | default(concatenations) -> []. 121 | 122 | normalize_paths(Paths, Basedir) -> 123 | lists:foldr(fun(X, Acc) -> [normalize_path(X, Basedir) | Acc] end, [], Paths). 124 | normalize_path(Path, Basedir) -> 125 | filename:join([Basedir, Path]). 126 | 127 | build_each([]) -> 128 | ok; 129 | build_each([{Destination, Sources, ConcatOptions} | Rest]) -> 130 | case any_needs_concat(Sources, Destination) of 131 | true -> 132 | Contents = concatenate_files(Sources), 133 | case file:write_file(Destination, Contents, [write]) of 134 | ok -> 135 | io:format("Built asset ~s~n", [Destination]), 136 | case lists:member(uglify, ConcatOptions) of 137 | true -> 138 | uglify(Destination); 139 | false -> 140 | ok 141 | end; 142 | {error, Reason} -> 143 | rebar_log:log(error, "Building asset ~s failed:~n ~p~n", 144 | [Destination, Reason]), 145 | rebar_utils:abort() 146 | end; 147 | false -> 148 | ok 149 | end, 150 | build_each(Rest). 151 | 152 | uglify(Source) -> 153 | Destination = uglify_destination(Source), 154 | rebar_js_uglifier_plugin:compress(Source, Destination, []). 155 | 156 | uglify_destination(Source) -> 157 | Outdir = filename:dirname(Source), 158 | Basename = filename:basename(Source, ".js"), 159 | normalize_path(lists:flatten(Basename ++ ".min.js"), Outdir). 160 | 161 | read(File) -> 162 | case file:read_file(File) of 163 | {ok, Binary} -> 164 | Binary; 165 | {error, Reason} -> 166 | rebar_log:log(error, "Reading asset ~s failed during concatenation:~n ~p~n", 167 | [File, Reason]), 168 | rebar_utils:abort() 169 | end. 170 | 171 | any_needs_concat(Sources, Destination) -> 172 | lists:any(fun(X) -> needs_concat(X, Destination) end, Sources). 173 | needs_concat(Source, Destination) -> 174 | filelib:last_modified(Destination) < filelib:last_modified(Source). 175 | 176 | delete_each([]) -> 177 | ok; 178 | delete_each([First | Rest]) -> 179 | case file:delete(First) of 180 | ok -> 181 | ok; 182 | {error, enoent} -> 183 | ok; 184 | {error, Reason} -> 185 | rebar_log:log(error, "Failed to delete ~s: ~p\n", [First, Reason]) 186 | end, 187 | delete_each(Rest). 188 | 189 | -ifdef(TEST). 190 | 191 | concatenate_test() -> 192 | ListSource1 = "ping", 193 | ListSource2 = "pong", 194 | ListOutput = concatenate([ListSource1, ListSource2]), 195 | ?assertEqual(<<"pingpong">>, ListOutput), 196 | BinarySource1 = <<"ping">>, 197 | BinarySource2 = <<"pong">>, 198 | BinaryOutput = concatenate([BinarySource1, BinarySource2]), 199 | ?assertEqual(<<"pingpong">>, BinaryOutput). 200 | 201 | -endif. 202 | -------------------------------------------------------------------------------- /plugins/rebar_js_handlebars_plugin.erl: -------------------------------------------------------------------------------- 1 | %% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*- 2 | %% ex: ts=4 sw=4 et 3 | %% ------------------------------------------------------------------- 4 | %% 5 | %% rebar: Erlang Build Tools 6 | %% 7 | %% Copyright (c) 2012 Christopher Meiklejohn (christopher.meiklejohn@gmail.com) 8 | %% 9 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 10 | %% of this software and associated documentation files (the "Software"), to deal 11 | %% in the Software without restriction, including without limitation the rights 12 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | %% copies of the Software, and to permit persons to whom the Software is 14 | %% furnished to do so, subject to the following conditions: 15 | %% 16 | %% The above copyright notice and this permission notice shall be included in 17 | %% all copies or substantial portions of the Software. 18 | %% 19 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | %% THE SOFTWARE. 26 | %% ------------------------------------------------------------------- 27 | 28 | %% The rebar_js_handlebars_plugin module is a plugin for rebar that 29 | %% wraps templates in compiler statements. 30 | %% 31 | %% Configuration options should be placed in rebar.config under 32 | %% 'js_handlebars'. Available options include: 33 | %% 34 | %% doc_root: where to find javascript files to concatenate 35 | %% "priv/assets/javascripts" by default 36 | %% 37 | %% out_dir: where to put concatenated javascript files 38 | %% "priv/www/javascripts" by default 39 | %% 40 | %% templates: list of tuples of format {Destination, [Sources]} 41 | %% empty list by default. 42 | %% 43 | %% source_ext: file extension to truncate to derive module name 44 | %% ".hbs" by default 45 | %% 46 | %% The default settings are the equivalent of: 47 | %% 48 | %% {js_handlebars, [ 49 | %% {doc_root, "priv/assets/javascripts"}, 50 | %% {out_dir, "priv/www/javascripts"}, 51 | %% {target, "Ember.TEMPLATES"}, 52 | %% {compiler, "Ember.Handlebars.compile"}, 53 | %% {source_ext,".hbs"}, 54 | %% {templates, []} 55 | %% ]}. 56 | %% 57 | %% An example of compiling a series of templates files: 58 | %% 59 | %% {js_handlebars, [ 60 | %% {doc_root, "priv/assets/javascripts"}, 61 | %% {out_dir, "priv/www/javascripts"}, 62 | %% {target, "Ember.TEMPLATES"}, 63 | %% {compiler, "Ember.Handlebars.compile"}, 64 | %% {templates, [{"templates.js", ["sidebar.hbs", "application.hbs"]}]} 65 | %% ]}. 66 | %% 67 | 68 | -module(rebar_js_handlebars_plugin). 69 | 70 | -ifdef(TEST). 71 | -include_lib("eunit/include/eunit.hrl"). 72 | -endif. 73 | 74 | -export([compile/2, 75 | clean/2]). 76 | 77 | -export([handlebars/4]). 78 | 79 | %% =================================================================== 80 | %% Public API 81 | %% =================================================================== 82 | 83 | compile(Config, _AppFile) -> 84 | Options = options(Config), 85 | OutDir = option(out_dir, Options), 86 | DocRoot = option(doc_root, Options), 87 | Templates = option(templates, Options), 88 | Targets = [{normalize_path(Destination, OutDir), 89 | normalize_paths(Sources, DocRoot), Options} || {Destination, Sources} <- Templates], 90 | build_each(Targets). 91 | 92 | clean(Config, _AppFile) -> 93 | Options = options(Config), 94 | OutDir = option(out_dir, Options), 95 | Templates = option(templates, Options), 96 | Targets = [normalize_path(Destination, OutDir) || {Destination, _} <- Templates], 97 | delete_each(Targets). 98 | 99 | %% @spec handlebars(list(), list(), list(), list()) -> binary() 100 | %% @doc Generate a handlebars compiler line. 101 | handlebars(Name, Body, Target, Compiler) -> 102 | Targeted = lists:flatten([Target, "['" ++ ensure_list(Name) ++ "']"]), 103 | Compiled = lists:flatten([Compiler, "('" ++ ensure_list(Body) ++ "');\n"]), 104 | list_to_binary(lists:flatten([Targeted, " = " ++ Compiled])). 105 | 106 | %% =================================================================== 107 | %% Internal functions 108 | %% =================================================================== 109 | 110 | options(Config) -> 111 | rebar_config:get_local(Config, js_handlebars, []). 112 | 113 | option(Option, Options) -> 114 | proplists:get_value(Option, Options, default(Option)). 115 | 116 | default(doc_root) -> "priv/assets/javascripts"; 117 | default(out_dir) -> "priv/www/javascripts"; 118 | default(target) -> "Ember.TEMPLATES"; 119 | default(compiler) -> "Ember.Handlebars.compile"; 120 | default(source_ext)-> ".hbs"; 121 | default(templates) -> []. 122 | 123 | ensure_list(Object) -> 124 | case is_binary(Object) of 125 | true -> 126 | binary_to_list(Object); 127 | false -> 128 | Object 129 | end. 130 | 131 | read(File) -> 132 | case file:read_file(File) of 133 | {ok, Binary} -> 134 | list_to_binary(re:replace(binary_to_list(Binary), "\\n+", "", 135 | [global])); 136 | {error, Reason} -> 137 | rebar_log:log(error, "Reading asset ~s failed during compilation:~n ~p~n", 138 | [File, Reason]), 139 | rebar_utils:abort() 140 | end. 141 | 142 | normalize_paths(Paths, Basedir) -> 143 | lists:foldr(fun(X, Acc) -> [normalize_path(X, Basedir) | Acc] end, [], Paths). 144 | normalize_path(Path, Basedir) -> 145 | filename:join([Basedir, Path]). 146 | 147 | build_each([]) -> 148 | ok; 149 | build_each([{Destination, Sources, Options} | Rest]) -> 150 | Target = option(target, Options), 151 | Compiler = option(compiler, Options), 152 | SourceExt = option(source_ext, Options), 153 | Contents = [handlebars(filename:basename(Source, SourceExt), read(Source), Target, Compiler) 154 | || Source <- Sources], 155 | Concatenated = rebar_js_concatenator_plugin:concatenate(Contents), 156 | case file:write_file(Destination, Concatenated, [write]) of 157 | ok -> 158 | io:format("Compiled handlebars asset ~s~n", [Destination]); 159 | {error, Reason} -> 160 | rebar_log:log(error, "Handlebars compliation of ~s failed:~n ~p~n", 161 | [Destination, Reason]), 162 | rebar_utils:abort() 163 | end, 164 | build_each(Rest). 165 | 166 | delete_each([]) -> 167 | ok; 168 | delete_each([First | Rest]) -> 169 | case file:delete(First) of 170 | ok -> 171 | ok; 172 | {error, enoent} -> 173 | ok; 174 | {error, Reason} -> 175 | rebar_log:log(error, "Failed to delete ~s: ~p\n", [First, Reason]) 176 | end, 177 | delete_each(Rest). 178 | 179 | %% =================================================================== 180 | %% Tests 181 | %% =================================================================== 182 | 183 | -ifdef(TEST). 184 | 185 | handlebars_test() -> 186 | Expected = <<"Ember.TEMPLATES['foo'] = Ember.Handlebars.compile('

bar

');\n">>, 187 | ListName = "foo", 188 | ListBody = "

bar

", 189 | ListTarget = "Ember.TEMPLATES", 190 | ListCompiler = "Ember.Handlebars.compile", 191 | ListOutput = handlebars(ListName, ListBody, ListTarget, ListCompiler), 192 | ?assertEqual(Expected, ListOutput), 193 | 194 | BinaryName = list_to_binary(ListName), 195 | BinaryBody = list_to_binary(ListBody), 196 | BinaryTarget = list_to_binary(ListTarget), 197 | BinaryCompiler = list_to_binary(ListCompiler), 198 | BinaryOutput = handlebars(BinaryName, BinaryBody, BinaryTarget, BinaryCompiler), 199 | ?assertEqual(Expected, BinaryOutput). 200 | 201 | -endif. 202 | -------------------------------------------------------------------------------- /plugins/rebar_js_stylus_plugin.erl: -------------------------------------------------------------------------------- 1 | %% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*- 2 | %% ex: ts=4 sw=4 et 3 | %% ------------------------------------------------------------------- 4 | %% 5 | %% rebar: Erlang Build Tools 6 | %% 7 | %% Copyright (c) 2012 Christopher Meiklejohn (cmeiklejohn@basho.com) 8 | %% 9 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 10 | %% of this software and associated documentation files (the "Software"), to deal 11 | %% in the Software without restriction, including without limitation the rights 12 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | %% copies of the Software, and to permit persons to whom the Software is 14 | %% furnished to do so, subject to the following conditions: 15 | %% 16 | %% The above copyright notice and this permission notice shall be included in 17 | %% all copies or substantial portions of the Software. 18 | %% 19 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | %% THE SOFTWARE. 26 | %% ------------------------------------------------------------------- 27 | 28 | %% The rebar_js_stylus_plugin module is a plugin for rebar that 29 | %% preprocesses stylus stylesheet files. 30 | %% 31 | %% Configuration options should be placed in rebar.config under 32 | %% 'js_stylus'. Available options include: 33 | %% 34 | %% stylus_path: path to the uglify executable 35 | %% "/usr/local/bin/stylus" by default 36 | %% 37 | %% doc_root: where to find .styl files to compile 38 | %% "priv/assets/stylesheets" by default 39 | %% 40 | %% out_dir: where to put preprocessed css files 41 | %% "priv/www/stylesheets" by default 42 | %% 43 | %% stylesheets: stylus files to compile 44 | %% [] by default 45 | %% 46 | %% The default settings are the equivalent of: 47 | %% {js_stylus, [ 48 | %% {stylus, "/usr/local/bin/stylus"}, 49 | %% {doc_root, "priv/assets/stylesheets"}, 50 | %% {out_dir, "priv/www/stylesheets"} 51 | %% {stylesheets, []} 52 | %% ]}. 53 | %% 54 | 55 | -module(rebar_js_stylus_plugin). 56 | 57 | -ifdef(TEST). 58 | -include_lib("eunit/include/eunit.hrl"). 59 | -endif. 60 | 61 | -export([compile/2, 62 | clean/2]). 63 | 64 | %% =================================================================== 65 | %% Public API 66 | %% =================================================================== 67 | 68 | 69 | compile(Config, _AppFile) -> 70 | Options = options(Config), 71 | DocRoot = option(doc_root, Options), 72 | Stylesheets = option(stylesheets, Options), 73 | Targets = [normalize_path(Stylesheet, DocRoot) 74 | || Stylesheet <- Stylesheets], 75 | process(Targets, Options). 76 | 77 | process([], _Options) -> 78 | ok; 79 | process(Targets, Options) -> 80 | OutDir = option(out_dir, Options), 81 | DocRoot = option(doc_root, Options), 82 | Stylus = option(stylus_path, Options), 83 | case stylus_is_present(Stylus) of 84 | true -> 85 | Cmd = lists:flatten([Stylus, " -o ", OutDir, " ", 86 | string:join(Targets, " ")]), 87 | ShOpts = [{use_stdout, false}, return_on_error], 88 | case rebar_utils:sh(Cmd, ShOpts) of 89 | {ok, _} -> 90 | io:format("Creating stylesheet assets in ~s~n", [OutDir]), 91 | ok; 92 | {error, Reason} -> 93 | rebar_log:log(error, "Stylus asset processing failed failed:~n ~p~n", 94 | [Reason]), 95 | rebar_utils:abort() 96 | end, 97 | ok; 98 | false -> 99 | rebar_log:log(warn, 100 | "Bypassing stylesheet processing of ~s: stylus missing.~n", [DocRoot]), 101 | ok 102 | end. 103 | 104 | clean(_Config, _AppFile) -> 105 | ok. 106 | 107 | %% =================================================================== 108 | %% Internal functions 109 | %% =================================================================== 110 | 111 | options(Config) -> 112 | rebar_config:get_local(Config, js_stylus, []). 113 | 114 | option(Option, Options) -> 115 | proplists:get_value(Option, Options, default(Option)). 116 | 117 | default(doc_root) -> "priv/assets/stylesheets"; 118 | default(out_dir) -> "priv/www/stylesheets"; 119 | default(stylus_path) -> "/usr/local/bin/stylus"; 120 | default(stylesheets) -> []. 121 | 122 | normalize_path(Path, Basedir) -> filename:join([Basedir, Path]). 123 | 124 | stylus_is_present(Stylus) -> filelib:is_file(Stylus). 125 | -------------------------------------------------------------------------------- /priv/admin/css/cluster.styl: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | CSS to be applied ONLY on the cluster page 4 | */ 5 | 6 | @import reusable 7 | 8 | 9 | #cluster-page 10 | 11 | .warning 12 | display : block 13 | padding : 10px 0 0 14 | color : riakred 15 | 16 | #current-area, #planned-area 17 | float : left 18 | width : 47% 19 | 20 | #current-area 21 | margin : 0 0 45px 0 22 | 23 | 24 | #area-separator 25 | width : 6% 26 | height : 450px 27 | float : left 28 | text-align : center 29 | 30 | div 31 | margin : 0 auto 32 | width : 1px 33 | height : 100% 34 | background : lightWhite 35 | 36 | span.gui-text-flat 37 | line-height : 1.5 38 | 39 | h3 40 | copy-font() 41 | 42 | .actions-container 43 | css-transition(height .4s ease, margin-bottom .4s ease) 44 | overflow : hidden 45 | height : 0 46 | margin-bottom : 0 47 | 48 | .open .actions-container 49 | css-transition(height .4s ease, margin-bottom .4s ease) 50 | height : 325px 51 | margin-bottom : 25px 52 | 53 | .open .node.taller .actions-container 54 | height : 375px 55 | 56 | .gui-light 57 | vertical-align : middle 58 | padding : 9px 5px 9px 14px 59 | margin-left : -6px 60 | margin-top : -7px 61 | 62 | .field-container 63 | width : 75% 64 | vertical-align : middle 65 | 66 | .gui-field 67 | margin-right : 0 68 | overflow : hidden 69 | 70 | #add-node 71 | padding-top : 40px 72 | margin-bottom : 45px 73 | 74 | .add-node-table tr td 75 | padding-top : 25px 76 | 77 | .list-header 78 | min-height : 18px 79 | 80 | li 81 | float : left 82 | margin-bottom : 30px 83 | 84 | h4 85 | display : inline-block 86 | 87 | #node-list, #planned-list 88 | margin-top : 20px 89 | 90 | #node-list 91 | .item1 92 | width : 18% 93 | .item2 94 | width : 42% 95 | .item3 96 | width : 18% 97 | .item4 98 | width : 22% 99 | 100 | .node 101 | margin-bottom : 10px 102 | position : relative 103 | 104 | .node > div 105 | float : left 106 | 107 | .node > div.clear 108 | float : none 109 | 110 | .toggle-container 111 | overflow : visible 112 | padding : 0 113 | 114 | .actions-toggle 115 | dimensions(82px, 30px) 116 | position : relative 117 | overflow : visible 118 | padding : 0 119 | background : url('/admin/ui/images/actions-toggle-text.png') -5px top no-repeat rgba(0, 0, 0, .3) 120 | margin-top : 3px 121 | 122 | a 123 | dimensions(48px, 34px) 124 | css-transition(left .2s ease) 125 | position : absolute 126 | display : inline-block 127 | background : url('/admin/ui/images/actions-toggle-slider.png') left top no-repeat transparent 128 | top : 0 129 | left : -5px 130 | 131 | .open .actions-toggle a 132 | css-transition(left .2s ease) 133 | left : 38px 134 | 135 | .name-box 136 | overflow : visible 137 | 138 | .ring-pct, .used-memory, .action-name 139 | monospace(); 140 | 141 | .used-memory 142 | display : inline-block 143 | padding : 0 0 0 5px 144 | vertical-align : middle 145 | 146 | .action-name 147 | display : inline-block 148 | padding-top : 8px 149 | 150 | .replacing-box .field-container 151 | width : 100% 152 | 153 | #planned-list 154 | .item1 155 | width : 40% 156 | .item2 157 | width : 17% 158 | .item3 159 | width : 17% 160 | .item4 161 | width : 26% 162 | 163 | 164 | .plan-details-header 165 | margin-top : 30px 166 | padding : 10px 0 167 | 168 | .accept-plan 169 | position : relative 170 | padding : 15px 0 171 | margin-top : 15px 172 | 173 | .clear-plan-box 174 | border-top : 1px solid lightWhite 175 | margin-top : 25px 176 | padding-top : 25px 177 | 178 | a 179 | display : block 180 | margin-top : 15px 181 | margin-left : -5px 182 | 183 | .plan-details 184 | @extend .gui-text-flat 185 | copy-font() 186 | line-height : 1.5 187 | 188 | #commit-button 189 | absoluteRight(0, 12px) 190 | 191 | .button-column 192 | text-align : right 193 | 194 | .pct-box 195 | margin-top : 8px 196 | display : inline-block 197 | vertical-align : middle 198 | 199 | .pct-arrows 200 | dimensions(19px, 33px) 201 | background : url('/admin/ui/images/pct-arrows-sprite.png') no-repeat transparent 202 | margin : 0 5px 0 0 203 | display : inline-block 204 | vertical-align : middle 205 | 206 | .pct-gaining 207 | background-position : -19px top 208 | 209 | .pct-losing 210 | background-position : -38px top 211 | 212 | .pct-static 213 | background-position : left top 214 | 215 | .green-pct-arrow 216 | dimensions(19px, 33px) 217 | background : url('/admin/ui/images/pct-arrows-sprite.png') no-repeat -57px top transparent 218 | margin : 0 5px 0 0 219 | display : none 220 | 221 | .error-message 222 | copy-font() 223 | vendor('box-shadow', 0 2px 3px darkened) 224 | corners() 225 | font-size : 14px 226 | font-style : italic 227 | color : mediumGray 228 | padding : 8px 10px 9px 12px 229 | background : rgba(232, 78, 78, .5) 230 | border : 1px solid lighten(rgba(232, 78, 78, .5), 15%) 231 | 232 | .error-link 233 | headline-bold() 234 | padding-left : 5px 235 | 236 | &:hover 237 | text-decoration : underline 238 | 239 | .close-error 240 | dimensions(16px, 16px) 241 | opaque(.6) 242 | cursor : pointer 243 | display : inline-block 244 | float : right 245 | vertical-align : middle 246 | background : url('/admin/ui/images/close-error.png') left top no-repeat transparent 247 | 248 | &:hover 249 | opaque(1) 250 | 251 | 252 | .memory-box 253 | width : 175px 254 | 255 | .membar-bg 256 | corners() 257 | dimensions(79px, 30px) 258 | position : relative 259 | display : inline-block 260 | vertical-align : middle 261 | 262 | .membar-fg 263 | corners() 264 | gradient(rgba(255, 255, 255, .4), rgba(255, 255, 255, .05)) 265 | vendor('box-shadow', 0 1px 0 lightWhite) 266 | dimensions(100%, 26px) 267 | absoluteLeft(0, 0) 268 | width : 75px 269 | z-index : 5 270 | border : 2px solid #1d1d1d 271 | 272 | .mem-colors 273 | dimensions(75px, 26px) 274 | float : left 275 | margin : 2px 0 0 2px 276 | 277 | .membar-mem 278 | dimensions(auto, 26px) 279 | background : url('/admin/ui/images/mem-bar-overlay.png') left top repeat transparent 280 | float : left 281 | z-index : 1 282 | 283 | .non-erlang-mem 284 | dimensions(auto, 26px) 285 | background : url('/admin/ui/images/mem-bar-overlay.png') left top repeat transparent 286 | float : left 287 | z-index : 1 288 | background-color : rgba(65, 179, 243, .8) 289 | 290 | .erlang-mem 291 | dimensions(auto, 26px) 292 | background : url('/admin/ui/images/mem-bar-overlay.png') left top repeat transparent 293 | float : left 294 | z-index : 1 295 | background-color : rgba(70, 166, 55, .8) 296 | 297 | .unknown-mem 298 | dimensions(auto, 26px) 299 | background : url('/admin/ui/images/mem-bar-overlay.png') left top repeat transparent 300 | float : left 301 | z-index : 1 302 | background-color : #404040 303 | width : 100% 304 | 305 | .actions-pointer 306 | dimensions(72px, 13px) 307 | background : url('/admin/ui/images/pointer.png') center top no-repeat transparent 308 | margin-top : 8px 309 | 310 | .actions-box 311 | corners() 312 | width : 92% 313 | background : darkened 314 | margin-bottom : 15px 315 | border-bottom : 1px solid lightWhite 316 | padding : 18px 317 | margin-right : 33px 318 | position : relative 319 | 320 | & > h4 321 | display : block 322 | border-bottom : 1px solid lightWhite 323 | padding : 0 0 12px 324 | 325 | 326 | .actions-box > div > span 327 | copy-font() 328 | color : lightGray 329 | padding-left : 3px 330 | 331 | .extra-actions 332 | opaque(.32) 333 | padding-bottom : 25px 334 | height : 35px 335 | position : relative 336 | 337 | & > div 338 | float : left 339 | 340 | .gui-checkbox-wrapper 341 | margin-left : 15px 342 | 343 | &.active 344 | opaque(1) 345 | 346 | .replacement-controls 347 | width : auto 348 | margin-top : 18px 349 | border-bottom : 1px solid lightWhite 350 | margin-bottom : 10px 351 | 352 | div 353 | margin-bottom : 8px 354 | 355 | select 356 | margin-top : -38px 357 | 358 | .replacement-node-dropdown 359 | margin-bottom : 5px 360 | 361 | .right-angle-arrow 362 | dimensions(15px, 18px) 363 | background : url('/admin/ui/images/right-angle-arrow.png') left top no-repeat transparent 364 | margin : 0 10px 0 10px 365 | 366 | .disabler 367 | background : red 368 | opaque(.2) 369 | absoluteLeft(0, 0) 370 | dimensions(100%, 100%) 371 | background : transparent 372 | 373 | .stage-button 374 | float : right 375 | 376 | .stage-instructions 377 | float : left 378 | display : block 379 | width : 50% 380 | 381 | .no-joining-nodes 382 | padding-top : 6px 383 | 384 | .slide-up 385 | margin-top : -35px 386 | 387 | .semi-transparent 388 | opaque(.32) 389 | 390 | 391 | /* RESPONSIVE MEDIA QUERIES */ 392 | 393 | @media screen and (max-width : 1200px) 394 | 395 | #area-separator 396 | display : none 397 | 398 | #current-area, #planned-area 399 | width : 100% 400 | 401 | #planned-area 402 | padding-top : 30px 403 | border-top : 1px solid lightWhite 404 | 405 | .field-container 406 | width : 90% 407 | 408 | #node-list 409 | .item1 410 | width : 100px 411 | .item2 412 | width : 500px 413 | .item3 414 | width : 100px 415 | .item4 416 | width : 150px 417 | 418 | #planned-list 419 | .item1 420 | width : 300px 421 | .item2 422 | width : 100px 423 | margin-left : 25px 424 | .item3 425 | width : 100px 426 | .item4 427 | width : 350px 428 | 429 | @media screen and (max-width : 950px) 430 | 431 | #node-list 432 | .item1 433 | width : 100px 434 | .item2 435 | width : 400px 436 | .item3 437 | width : 100px 438 | .item4 439 | width : 150px 440 | 441 | #planned-list 442 | .item1 443 | width : 275px 444 | .item2 445 | width : 100px 446 | .item3 447 | width : 100px 448 | .item4 449 | width : 225px 450 | 451 | @media screen and (max-width : 850px) 452 | 453 | #node-list 454 | .item1 455 | width : 100px 456 | .item2 457 | width : 300px 458 | 459 | .field-container 460 | width : 75% 461 | 462 | .item3 463 | width : 100px 464 | .item4 465 | width : 150px 466 | 467 | #planned-list 468 | .item3 469 | display : none 470 | 471 | .used-memory 472 | display : none 473 | 474 | @media screen and (max-width : 700px) 475 | 476 | #node-list 477 | .item1 478 | width : 100px 479 | .item2 480 | width : 200px 481 | 482 | .field-container 483 | width : 75% 484 | 485 | .item3 486 | width : 100px 487 | .item4 488 | width : 150px 489 | 490 | #planned-list 491 | .item1 492 | width : 200px 493 | .item2 494 | width : 100px 495 | margin-left : 0 496 | .item4 497 | width : 200px 498 | 499 | .gui-light 500 | display : none 501 | 502 | @media screen and (max-width : 600px) 503 | 504 | #node-list 505 | .item1 506 | display : none 507 | .item2 508 | width : 40% 509 | .field-container 510 | width : 95% 511 | .item4 512 | width : 100px 513 | 514 | #planned-list 515 | .item1 516 | width : 40% 517 | .field-container 518 | width : 95% 519 | .item2 520 | width : 20% 521 | margin-right : 2% 522 | .item4 523 | width : 38% 524 | .field-container 525 | width : 95% 526 | 527 | .actions-container 528 | display : none 529 | -------------------------------------------------------------------------------- /priv/admin/css/fonts.styl: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | @font-face styles 4 | */ 5 | 6 | @font-face { 7 | font-family: 'noticia'; 8 | src: url('/admin/ui/fonts/noticiatext-italic-webfont.eot'); 9 | src: url('/admin/ui/fonts/noticiatext-italic-webfont.eot?#iefix') format('embedded-opentype'), 10 | url('/admin/ui/fonts/noticiatext-italic-webfont.woff') format('woff'), 11 | url('/admin/ui/fonts/noticiatext-italic-webfont.ttf') format('truetype'), 12 | url('/admin/ui/fonts/noticiatext-italic-webfont.svg#noticia_textitalic') format('svg'); 13 | font-weight: normal; 14 | font-style: italic; 15 | 16 | } 17 | 18 | @font-face { 19 | font-family: 'noticia'; 20 | src: url('/admin/ui/fonts/noticiatext-regular-webfont.eot'); 21 | src: url('/admin/ui/fonts/noticiatext-regular-webfont.eot?#iefix') format('embedded-opentype'), 22 | url('/admin/ui/fonts/noticiatext-regular-webfont.woff') format('woff'), 23 | url('/admin/ui/fonts/noticiatext-regular-webfont.ttf') format('truetype'), 24 | url('/admin/ui/fonts/noticiatext-regular-webfont.svg#noticia_textregular') format('svg'); 25 | font-weight: normal; 26 | font-style: normal; 27 | 28 | } 29 | 30 | @font-face { 31 | font-family: 'titillium'; 32 | src: url('/admin/ui/fonts/titilliumtext25l002-webfont.eot'); 33 | src: url('/admin/ui/fonts/titilliumtext25l002-webfont.eot?#iefix') format('embedded-opentype'), 34 | url('/admin/ui/fonts/titilliumtext25l002-webfont.woff') format('woff'), 35 | url('/admin/ui/fonts/titilliumtext25l002-webfont.ttf') format('truetype'), 36 | url('/admin/ui/fonts/titilliumtext25l002-webfont.svg#titilliumtext25l600_wt') format('svg'); 37 | font-weight: bold; 38 | font-style: normal; 39 | 40 | } 41 | 42 | @font-face { 43 | font-family: 'titillium'; 44 | src: url('/admin/ui/fonts/titilliumtext25l005-webfont.eot'); 45 | src: url('/admin/ui/fonts/titilliumtext25l005-webfont.eot?#iefix') format('embedded-opentype'), 46 | url('/admin/ui/fonts/titilliumtext25l005-webfont.woff') format('woff'), 47 | url('/admin/ui/fonts/titilliumtext25l005-webfont.ttf') format('truetype'), 48 | url('/admin/ui/fonts/titilliumtext25l005-webfont.svg#titilliumtext25l1_wt') format('svg'); 49 | font-weight: normal; 50 | font-style: normal; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /priv/admin/css/nodes.styl: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | CSS to be applied ONLY on the cluster page 4 | */ 5 | 6 | @import reusable 7 | 8 | 9 | #nodes-page 10 | 11 | .right 12 | float : right 13 | 14 | .gui-point-button-right, .gui-rect-button 15 | margin-right : 10px 16 | margin-top : 35px 17 | 18 | #current-area 19 | max-width : 850px 20 | margin : 0 auto 45px auto 21 | padding-top : 40px 22 | 23 | & > span.gui-text-flat 24 | line-height : 1.5 25 | display : block 26 | 27 | h3 28 | copy-font() 29 | 30 | .spinner-box 31 | corners() 32 | text-align : center 33 | background : rgba(255, 255, 255, .05) 34 | padding : 25px 35 | 36 | img, h4 37 | vertical-align : middle 38 | 39 | h4 40 | opaque(.8) 41 | display : inline-block 42 | padding-top : 6px 43 | padding-left : 10px 44 | line-height : 1.2 45 | 46 | .gui-light 47 | vertical-align : middle 48 | padding : 9px 5px 9px 14px 49 | margin-left : -6px 50 | margin-top : -7px 51 | 52 | .field-container 53 | width : 86% 54 | vertical-align : middle 55 | 56 | .gui-field 57 | margin-right : 0 58 | overflow : hidden 59 | 60 | .list-header 61 | min-height : 18px 62 | 63 | li 64 | float : left 65 | margin-bottom : 30px 66 | 67 | h4 68 | display : inline-block 69 | 70 | #node-list 71 | margin-top : 20px 72 | 73 | .item1 74 | width : 6% 75 | .item2 76 | width : 10% 77 | .item3 78 | width : 56% 79 | .item4 80 | width : 13% 81 | .item5 82 | width : 15% 83 | 84 | 85 | 86 | 87 | 88 | .node 89 | margin-bottom : 10px 90 | position : relative 91 | 92 | .node > div 93 | float : left 94 | 95 | .node > div.clear 96 | float : none 97 | 98 | .node 99 | 100 | .gui-radio-wrapper 101 | dimensions(25px, 24px) 102 | text-align : center 103 | display : inline-block 104 | position : relative 105 | 106 | .disabler 107 | dimensions(100%, 100%) 108 | absoluteLeft(0, 0); 109 | opaque(0) 110 | display : none 111 | 112 | &.show 113 | display : block 114 | 115 | .item1 .gui-radio-wrapper 116 | margin-left : 4px 117 | 118 | .item2 .gui-radio-wrapper 119 | margin-left : 8px 120 | 121 | input[type=radio] 122 | display : inline 123 | float : none 124 | 125 | .name-box 126 | overflow : visible 127 | 128 | .ring-pct, .used-memory, .action-name 129 | monospace(); 130 | 131 | .used-memory 132 | display : inline-block 133 | padding : 0 0 0 5px 134 | vertical-align : middle 135 | 136 | .pct-box 137 | margin-top : 8px 138 | display : inline-block 139 | vertical-align : middle 140 | 141 | .pct-arrows 142 | dimensions(19px, 33px) 143 | background : url('/admin/ui/images/pct-arrows-sprite.png') no-repeat transparent 144 | margin : 0 5px 0 0 145 | display : inline-block 146 | vertical-align : middle 147 | 148 | .pct-gaining 149 | background-position : -19px top 150 | 151 | .pct-losing 152 | background-position : -38px top 153 | 154 | .pct-static 155 | background-position : left top 156 | 157 | .green-pct-arrow 158 | dimensions(19px, 33px) 159 | background : url('/admin/ui/images/pct-arrows-sprite.png') no-repeat -57px top transparent 160 | margin : 0 5px 0 0 161 | display : none 162 | 163 | .memory-box 164 | width : 175px 165 | 166 | .membar-bg 167 | corners() 168 | dimensions(79px, 30px) 169 | position : relative 170 | display : inline-block 171 | vertical-align : middle 172 | 173 | .membar-fg 174 | corners() 175 | gradient(rgba(255, 255, 255, .4), rgba(255, 255, 255, .05)) 176 | vendor('box-shadow', 0 1px 0 lightWhite) 177 | dimensions(100%, 26px) 178 | absoluteLeft(0, 0) 179 | width : 75px 180 | z-index : 5 181 | border : 2px solid #1d1d1d 182 | 183 | .mem-colors 184 | dimensions(75px, 26px) 185 | float : left 186 | margin : 2px 0 0 2px 187 | 188 | .membar-mem 189 | dimensions(auto, 26px) 190 | background : url('/admin/ui/images/mem-bar-overlay.png') left top repeat transparent 191 | float : left 192 | z-index : 1 193 | 194 | .non-erlang-mem 195 | dimensions(auto, 26px) 196 | background : url('/admin/ui/images/mem-bar-overlay.png') left top repeat transparent 197 | float : left 198 | z-index : 1 199 | background-color : rgba(65, 179, 243, .8) 200 | 201 | .erlang-mem 202 | dimensions(auto, 26px) 203 | background : url('/admin/ui/images/mem-bar-overlay.png') left top repeat transparent 204 | float : left 205 | z-index : 1 206 | background-color : rgba(70, 166, 55, .8) 207 | 208 | .unknown-mem 209 | dimensions(auto, 26px) 210 | background : url('/admin/ui/images/mem-bar-overlay.png') left top repeat transparent 211 | float : left 212 | z-index : 1 213 | background-color : #404040 214 | width : 100% 215 | 216 | 217 | .slide-up 218 | margin-top : -35px 219 | 220 | .semi-transparent 221 | opaque(.32) 222 | 223 | 224 | /* RESPONSIVE MEDIA QUERIES */ 225 | 226 | @media screen and (max-width : 800px) 227 | 228 | #node-list 229 | .item1 230 | width : 8% 231 | .item2 232 | width : 10% 233 | .item3 234 | width : 49% 235 | .item4 236 | width : 15% 237 | .item5 238 | width : 18% 239 | 240 | .field-container 241 | width : 75% 242 | 243 | 244 | @media screen and (max-width : 670px) 245 | 246 | #node-list 247 | .memory-box 248 | padding-top : 7px 249 | 250 | .membar-bg 251 | display : none 252 | 253 | @media screen and (max-width : 570px) 254 | 255 | #node-list 256 | .item1 257 | width : 12% 258 | .item2 259 | width : 15% 260 | .item3 261 | width : 53% 262 | .item4 263 | display : none 264 | .item5 265 | width : 20% 266 | 267 | @media screen and (max-width : 400px) 268 | 269 | #node-list 270 | .item1 271 | width : 12% 272 | .item2 273 | width : 15% 274 | .item3 275 | width : 47% 276 | .item4 277 | display : none 278 | .item5 279 | width : 15% 280 | 281 | 282 | 283 | 284 | 285 | -------------------------------------------------------------------------------- /priv/admin/css/reset.styl: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | http://meyerweb.com/eric/tools/css/reset/ 4 | v2.0 | 20110126 5 | License: none (public domain) 6 | */ 7 | 8 | html, body, div, span, applet, object, iframe, 9 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 10 | a, abbr, acronym, address, big, cite, code, 11 | del, dfn, em, img, ins, kbd, q, s, samp, 12 | small, strike, strong, sub, sup, tt, var, 13 | b, u, i, center, 14 | dl, dt, dd, ol, ul, li, 15 | fieldset, form, label, legend, 16 | table, caption, tbody, tfoot, thead, tr, th, td, 17 | article, aside, canvas, details, embed, 18 | figure, figcaption, footer, header, hgroup, 19 | menu, nav, output, ruby, section, summary, 20 | time, mark, audio, video { 21 | margin: 0; 22 | padding: 0; 23 | border: 0; 24 | font-size: 100%; 25 | font: inherit; 26 | vertical-align: baseline; 27 | } 28 | /* HTML5 display-role reset for older browsers */ 29 | article, aside, details, figcaption, figure, 30 | footer, header, hgroup, menu, nav, section { 31 | display: block; 32 | } 33 | body { 34 | line-height: 1; 35 | } 36 | ol, ul { 37 | list-style: none; 38 | } 39 | blockquote, q { 40 | quotes: none; 41 | } 42 | blockquote:before, blockquote:after, 43 | q:before, q:after { 44 | content: ''; 45 | content: none; 46 | } 47 | table { 48 | border-collapse: collapse; 49 | border-spacing: 0; 50 | } 51 | td { 52 | vertical-align: top; 53 | } 54 | -------------------------------------------------------------------------------- /priv/admin/css/reusable.styl: -------------------------------------------------------------------------------- 1 | 2 | /* Variables */ 3 | coverup = rgba(0, 0, 0, .85) 4 | darkened = rgba(0, 0, 0, .35) 5 | darkGray = #2d2d2d 6 | mediumGray = #cccccc 7 | lightGray = #efefef 8 | lightWhite = rgba(255, 255, 255, .18) 9 | highlight = rgba(255, 255, 255, .08) 10 | offlight = #767676 11 | riakorange = #f99d33 12 | lightorange = lighten(riakorange, 33%) 13 | riakred = #f05f5f 14 | 15 | /* End Variables */ 16 | 17 | /* Functions */ 18 | vendor(prop, args) 19 | -webkit-{prop} : args 20 | -moz-{prop} : args 21 | -ms-{prop} : args 22 | -o-{prop} : args 23 | {prop} : args 24 | 25 | gradient(lightcolor, darkcolor) 26 | background : darkcolor 27 | background : -moz-linear-gradient( top, lightcolor 0%, darkcolor 100%) 28 | background : -webkit-gradient(linear, left top, left bottom, color-stop(0%, lightcolor), color-stop(100%, darkcolor)) 29 | background : -webkit-linear-gradient( top, lightcolor 0%, darkcolor 100%) 30 | background : -o-linear-gradient( top, lightcolor 0%, darkcolor 100%) 31 | background : -ms-linear-gradient( top, lightcolor 0%, darkcolor 100%) 32 | background : linear-gradient( top bottom, lightcolor 0%, darkcolor 100%) 33 | 34 | radialGradient(lightcolor, darkcolor) 35 | background-image : -moz-radial-gradient( center, lightcolor 0%, darkcolor 100%) 36 | background-image : -webkit-gradient(radial, center, color-stop(0%, lightcolor), color-stop(100%, darkcolor)) 37 | background-image : -webkit-radial-gradient( center, lightcolor 0%, darkcolor 100%) 38 | background-image : -o-radial-gradient( center, lightcolor 0%, darkcolor 100%) 39 | background-image : -ms-radial-gradient( center, lightcolor 0%, darkcolor 100%) 40 | background-image : radial-gradient( center, lightcolor 0%, darkcolor 100%) 41 | 42 | corners(curve = 4px) 43 | vendor('border-radius', curve) 44 | 45 | singleCorner(vert, horiz, args = 4px) 46 | -webkit-border-{vert}-{horiz}-radius : args 47 | -moz-border-radius-{vert}{horiz} : args 48 | border-{vert}-{horiz}-radius : args 49 | 50 | textStroke(args = 1px #fff) 51 | vendor('text-stroke', args) 52 | 53 | textShadow(args = 0 2px 0 #000) 54 | text-shadow : args 55 | 56 | opaque(dec = 1) 57 | opacity : dec 58 | filter : unquote('alpha(opacity=' + (dec * 100) + ')') 59 | 60 | repeatingBg(path, bgcolor = transparent) 61 | background : url(path) left top repeat bgcolor 62 | 63 | hSize(fs, ls, pb) // General stylings for h1, h2, h3, etc tags 64 | font-size : fs 65 | letter-spacing : ls 66 | padding-bottom : pb 67 | 68 | dimensions(wUnit = auto, hUnit = auto) 69 | width : wUnit 70 | height : hUnit 71 | 72 | absoluteLeft(lUnit, tUnit) 73 | position : absolute 74 | left : lUnit 75 | top : tUnit 76 | 77 | absoluteRight(rUnit, tUnit) 78 | position : absolute 79 | right : rUnit 80 | top : tUnit 81 | 82 | transition(args) 83 | -webkit-transition : args 84 | -moz-transition : args 85 | -o-transition : args 86 | transition : args 87 | 88 | headline-font() 89 | font-family : 'titillium', helvetica, arial, sans-serif 90 | 91 | copy-font() 92 | font-family : 'noticia', georgia, serif 93 | 94 | monospace() 95 | font-family : 'menlo', 'consolas', 'monaco', monospace 96 | 97 | copy-bold() 98 | copy-font() 99 | font-weight : bold 100 | 101 | headline-bold() 102 | headline-font() 103 | font-weight : bold 104 | 105 | // Applies a css transition 106 | css-transition(args) 107 | -webkit-transition : args 108 | -moz-transition : args 109 | -ms-transition : args 110 | transition : args 111 | 112 | // Applies a css transform 113 | css-transform(args) 114 | -webkit-transform : rotate(args) 115 | -moz-transform : rotate(args) 116 | -o-transform : rotate(args) 117 | transform : rotate(args) 118 | 119 | 120 | /* End Functions */ 121 | -------------------------------------------------------------------------------- /priv/admin/css/snapshot.styl: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | CSS to be applied ONLY on the snapshot page 4 | */ 5 | 6 | @import reusable 7 | 8 | #snapshot-page 9 | 10 | #healthy-cluster, #unhealthy-cluster 11 | padding-top : 25px 12 | 13 | section 14 | padding-left : 336px 15 | 16 | h2 17 | padding-top : 20px 18 | 19 | h3 20 | margin-top : 30px 21 | line-height : 1.5 22 | 23 | h4 24 | copy-font() 25 | margin-top : 20px 26 | font-size: 14px 27 | font-style: italic 28 | line-height : 1.5 29 | 30 | ul 31 | margin : 10px 0 0 20px 32 | 33 | li 34 | headline-font() 35 | line-height : 1.5 36 | font-size : 15px 37 | 38 | a 39 | color : lightGray 40 | 41 | #health-indicator 42 | float : left 43 | margin-right : 25px 44 | 45 | #healthy-cluster #health-indicator 46 | dimensions(337px, 310px) 47 | 48 | #unhealthy-cluster #health-indicator 49 | dimensions(311px, 309px) 50 | 51 | /* RESPONSIVE MEDIA QUERIES */ 52 | 53 | @media screen and (max-width : 899px) 54 | 55 | #healthy-cluster #health-indicator 56 | dimensions(279px, 249px) 57 | 58 | #unhealthy-cluster #health-indicator 59 | dimensions(250px, 249px) 60 | 61 | #healthy-cluster section, #unhealthy-cluster section 62 | padding-left : 275px 63 | 64 | @media screen and (max-width : 499px) 65 | 66 | .health-info 67 | text-align : center 68 | 69 | #health-indicator 70 | margin : 0 auto 71 | float : none 72 | display : block 73 | 74 | #healthy-cluster section, #unhealthy-cluster section 75 | padding-left : 0 76 | margin : 0 auto 77 | text-align : left 78 | 79 | 80 | /* END RESPONSIVE MEDIA QUERIES */ 81 | 82 | -------------------------------------------------------------------------------- /priv/admin/css/style.styl: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Main Stylus CSS File 4 | Imports and arranges all other stylus files. 5 | */ 6 | 7 | @import reusable 8 | @import reset 9 | @import fonts 10 | @import general 11 | @import snapshot 12 | @import cluster 13 | @import nodes 14 | @import ring 15 | -------------------------------------------------------------------------------- /priv/admin/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/favicon.ico -------------------------------------------------------------------------------- /priv/admin/fonts/noticiatext-italic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/fonts/noticiatext-italic-webfont.eot -------------------------------------------------------------------------------- /priv/admin/fonts/noticiatext-italic-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/fonts/noticiatext-italic-webfont.ttf -------------------------------------------------------------------------------- /priv/admin/fonts/noticiatext-italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/fonts/noticiatext-italic-webfont.woff -------------------------------------------------------------------------------- /priv/admin/fonts/noticiatext-regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/fonts/noticiatext-regular-webfont.eot -------------------------------------------------------------------------------- /priv/admin/fonts/noticiatext-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/fonts/noticiatext-regular-webfont.ttf -------------------------------------------------------------------------------- /priv/admin/fonts/noticiatext-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/fonts/noticiatext-regular-webfont.woff -------------------------------------------------------------------------------- /priv/admin/fonts/titilliumtext25l002-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/fonts/titilliumtext25l002-webfont.eot -------------------------------------------------------------------------------- /priv/admin/fonts/titilliumtext25l002-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/fonts/titilliumtext25l002-webfont.ttf -------------------------------------------------------------------------------- /priv/admin/fonts/titilliumtext25l002-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/fonts/titilliumtext25l002-webfont.woff -------------------------------------------------------------------------------- /priv/admin/fonts/titilliumtext25l005-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/fonts/titilliumtext25l005-webfont.eot -------------------------------------------------------------------------------- /priv/admin/fonts/titilliumtext25l005-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/fonts/titilliumtext25l005-webfont.ttf -------------------------------------------------------------------------------- /priv/admin/fonts/titilliumtext25l005-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/fonts/titilliumtext25l005-webfont.woff -------------------------------------------------------------------------------- /priv/admin/images/action-button-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/action-button-sprite.png -------------------------------------------------------------------------------- /priv/admin/images/actions-toggle-slider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/actions-toggle-slider.png -------------------------------------------------------------------------------- /priv/admin/images/actions-toggle-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/actions-toggle-text.png -------------------------------------------------------------------------------- /priv/admin/images/add-phase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/add-phase.png -------------------------------------------------------------------------------- /priv/admin/images/area-pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/area-pointer.png -------------------------------------------------------------------------------- /priv/admin/images/basho-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/basho-logo.png -------------------------------------------------------------------------------- /priv/admin/images/blue-rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/blue-rect.png -------------------------------------------------------------------------------- /priv/admin/images/checkbox-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/checkbox-sprite.png -------------------------------------------------------------------------------- /priv/admin/images/close-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/close-error.png -------------------------------------------------------------------------------- /priv/admin/images/cut-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/cut-bg.png -------------------------------------------------------------------------------- /priv/admin/images/dropdown-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/dropdown-bg.png -------------------------------------------------------------------------------- /priv/admin/images/dropdown-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/dropdown-left.png -------------------------------------------------------------------------------- /priv/admin/images/dropdown-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/dropdown-right.png -------------------------------------------------------------------------------- /priv/admin/images/expand.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/expand.gif -------------------------------------------------------------------------------- /priv/admin/images/field-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/field-bg.png -------------------------------------------------------------------------------- /priv/admin/images/fieldcap-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/fieldcap-left.png -------------------------------------------------------------------------------- /priv/admin/images/fieldcap-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/fieldcap-right.png -------------------------------------------------------------------------------- /priv/admin/images/gui-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/gui-bg.jpg -------------------------------------------------------------------------------- /priv/admin/images/header-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/header-bg.png -------------------------------------------------------------------------------- /priv/admin/images/healthy-cluster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/healthy-cluster.png -------------------------------------------------------------------------------- /priv/admin/images/hide-show-toggle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/hide-show-toggle.png -------------------------------------------------------------------------------- /priv/admin/images/leavecluster-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/leavecluster-sprite.png -------------------------------------------------------------------------------- /priv/admin/images/leaving-slider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/leaving-slider.png -------------------------------------------------------------------------------- /priv/admin/images/light-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/light-sprite.png -------------------------------------------------------------------------------- /priv/admin/images/markdown-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/markdown-sprite.png -------------------------------------------------------------------------------- /priv/admin/images/mem-bar-overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/mem-bar-overlay.png -------------------------------------------------------------------------------- /priv/admin/images/membar-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/membar-bg.png -------------------------------------------------------------------------------- /priv/admin/images/membar-fg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/membar-fg.png -------------------------------------------------------------------------------- /priv/admin/images/nav-icons-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/nav-icons-small.png -------------------------------------------------------------------------------- /priv/admin/images/nav-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/nav-icons.png -------------------------------------------------------------------------------- /priv/admin/images/node-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/node-bg.png -------------------------------------------------------------------------------- /priv/admin/images/orange-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/orange-arrow.png -------------------------------------------------------------------------------- /priv/admin/images/pct-arrows-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/pct-arrows-sprite.png -------------------------------------------------------------------------------- /priv/admin/images/pointbutton-right-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/pointbutton-right-sprite.png -------------------------------------------------------------------------------- /priv/admin/images/pointbutton-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/pointbutton-sprite.png -------------------------------------------------------------------------------- /priv/admin/images/pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/pointer.png -------------------------------------------------------------------------------- /priv/admin/images/powerbutton-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/powerbutton-sprite.png -------------------------------------------------------------------------------- /priv/admin/images/radio-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/radio-sprite.png -------------------------------------------------------------------------------- /priv/admin/images/rectbutton-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/rectbutton-sprite.png -------------------------------------------------------------------------------- /priv/admin/images/resizer-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/resizer-bg.png -------------------------------------------------------------------------------- /priv/admin/images/riak-control-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/riak-control-logo-small.png -------------------------------------------------------------------------------- /priv/admin/images/riak-control-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/riak-control-logo.png -------------------------------------------------------------------------------- /priv/admin/images/riak-transparent-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/riak-transparent-small.png -------------------------------------------------------------------------------- /priv/admin/images/right-angle-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/right-angle-arrow.png -------------------------------------------------------------------------------- /priv/admin/images/ring-indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/ring-indicator.png -------------------------------------------------------------------------------- /priv/admin/images/slider-groove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/slider-groove.png -------------------------------------------------------------------------------- /priv/admin/images/slider-handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/slider-handle.png -------------------------------------------------------------------------------- /priv/admin/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/spinner.gif -------------------------------------------------------------------------------- /priv/admin/images/stagebutton-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/stagebutton-sprite.png -------------------------------------------------------------------------------- /priv/admin/images/tooltip-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/tooltip-mark.png -------------------------------------------------------------------------------- /priv/admin/images/unhealthy-cluster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/images/unhealthy-cluster.png -------------------------------------------------------------------------------- /priv/admin/js/app.js: -------------------------------------------------------------------------------- 1 | minispade.register('app', function() { 2 | 3 | /** 4 | * RiakControl Ember application. 5 | */ 6 | RiakControl = Ember.Application.create(); 7 | 8 | /** 9 | * How often to refresh/poll the server for changes in cluster 10 | * and partition status 11 | */ 12 | RiakControl.refreshInterval = 1000; 13 | 14 | /** 15 | * Provide reload functionality for recordarrays. 16 | */ 17 | DS.RecordArray.reopen({ 18 | reload: function() { 19 | Ember.assert("Can only reload base RecordArrays", 20 | this.constructor === DS.RecordArray); 21 | var store = this.get('store'); 22 | store.findAll(this.get('type')); 23 | } 24 | }); 25 | 26 | /** 27 | * Data store configuration. 28 | */ 29 | RiakControl.Store = DS.Store.extend({ 30 | adapter: DS.RESTAdapter.reopen({ namespace: 'admin' }) 31 | }); 32 | 33 | RiakControl.store = RiakControl.Store.create(); 34 | 35 | // Automatically add CSRF tokens to AJAX requests. 36 | $(document).ajaxSend(function(elm, xhr, s){ 37 | var csrf_token = $('meta[name=csrf_token]').prop('content'); 38 | 39 | if(s.type === 'POST' || s.type === 'PUT') { 40 | xhr.setRequestHeader('X-CSRF-Token', csrf_token); 41 | } 42 | }); 43 | 44 | // Set default content-type for AJAX requests. 45 | // From: http://stackoverflow.com/questions/1749272/jquery-how-to-put-json-via-ajax 46 | $.ajaxSetup({ 47 | contentType: 'application/json', 48 | processData: false 49 | }); 50 | 51 | // Prefilter for stringifying. 52 | // From: http://stackoverflow.com/questions/1749272/jquery-how-to-put-json-via-ajax 53 | $.ajaxPrefilter(function(options, originalOptions, jqXHR) { 54 | if(options.data) { 55 | options.data = JSON.stringify(options.data); 56 | } 57 | }); 58 | 59 | minispade.require('core'); 60 | minispade.require('router'); 61 | minispade.require('shared'); 62 | minispade.require('snapshot'); 63 | minispade.require('cluster'); 64 | minispade.require('nodes'); 65 | minispade.require('ring'); 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /priv/admin/js/core.js: -------------------------------------------------------------------------------- 1 | minispade.register('core', function() { 2 | 3 | /** Handle an array type. */ 4 | var none = Ember.isNone; 5 | 6 | DS.ArrayTransform = DS.Transform.extend({ 7 | deserialize: function(serialized) { 8 | return none(serialized) ? [] : serialized; 9 | }, 10 | 11 | serialize: function(deserialized) { 12 | return none(deserialized) ? [] : deserialized; 13 | }, 14 | }); 15 | 16 | RiakControl.register('transform:array', DS.ArrayTransform); 17 | 18 | /** 19 | * @class 20 | * 21 | * Node models a Riak Node participating in a cluster. 22 | */ 23 | RiakControl.Node = DS.Model.extend( 24 | /** @scope RiakControl.Node.prototype */ { 25 | 26 | /** 27 | * Use the node name and ip address as the 28 | * unique identifier for the node. 29 | */ 30 | primaryKey: 'name', 31 | 32 | /** The id attribute contains the name after deserializing */ 33 | name: Ember.computed.alias('id'), 34 | 35 | status: DS.attr("string"), 36 | 37 | reachable: DS.attr("boolean"), 38 | 39 | ring_pct: DS.attr("number"), 40 | 41 | pending_pct: DS.attr("number"), 42 | 43 | mem_total: DS.attr("number"), 44 | 45 | mem_used: DS.attr("number"), 46 | 47 | mem_erlang: DS.attr("number"), 48 | 49 | low_mem: DS.attr("boolean"), 50 | 51 | /** 52 | * This boolean attribute determines if the node 53 | * responsible for the API requests is running 54 | * Riak Control. 55 | */ 56 | me: DS.attr("boolean"), 57 | 58 | /** 59 | * Whether this node is currently the claimant or not. 60 | */ 61 | claimant: DS.attr("boolean") 62 | }); 63 | 64 | /** Register serializer that respects custom primary key */ 65 | RiakControl.register('serializer:node', DS.RESTSerializer.extend({ 66 | primaryKey: 'name' 67 | })); 68 | }); 69 | -------------------------------------------------------------------------------- /priv/admin/js/generated/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/priv/admin/js/generated/.gitkeep -------------------------------------------------------------------------------- /priv/admin/js/legacy/gui.js: -------------------------------------------------------------------------------- 1 | // Framework for the cool UI tricks 2 | // 3 | // TODO: These should be moved to didInsertElement hooks on the 4 | // Ember Views once they are no longer needed by the pages scheduled 5 | // for deprecation. 6 | // 7 | // Set up a safe, global namespace in the jQuery object. 8 | $(document).ready(function() { 9 | $.riakControl = $.riakControl || {}; 10 | 11 | // MARK THE ACTIVE NAV ICON WITH THE PROPER CLASS 12 | $.riakControl.markNavActive = $.riakControl.markNavActive || function markNavActive(id) { 13 | Ember.run.next(function() { 14 | var listItems = $('nav li'), activeItem = $("#" + id); 15 | listItems.each(function (index, each) { 16 | if (each !== activeItem[0]) { 17 | $(each).removeClass('active'); 18 | } else { 19 | $(each).addClass('active'); 20 | } 21 | }); 22 | }); 23 | }; 24 | 25 | // MAKE CHECKBOXES WORK WHEN YOU CLICK THEM 26 | $(document).on('change', '.gui-checkbox', function(e) { 27 | var me = $(this), 28 | parent = me.parent(), 29 | checked = me.prop('checked'); 30 | 31 | if (checked) { 32 | parent.css('background-position', 'left bottom'); 33 | } else { 34 | parent.css('background-position', 'left top'); 35 | } 36 | }); 37 | 38 | // MAKE RADIO BUTTONS WORK WHEN YOU CLICK THEM 39 | $(document).on('change', '.gui-radio', function(e) { 40 | var me = $(this), 41 | parent = me.parent(), 42 | checked = me.prop('checked'), 43 | group = $('input[type="radio"][name="' + me.prop('name') + '"]'); 44 | 45 | /* 46 | * If the radio button is checked... 47 | */ 48 | if (checked) { 49 | /* 50 | * Change the position of the background image sprite. 51 | */ 52 | parent.css('background-position', 'left bottom'); 53 | /* 54 | * Loop over all other radio buttons in the group and set their 55 | * background positions to reflect the unchecked state. 56 | */ 57 | group.each(function (index, item) { 58 | var $item = $(item); 59 | if ($item[0] !== me[0]) { 60 | $item.parent().css('background-position', 'left top'); 61 | } 62 | }); 63 | /* 64 | * If the checked radio button is the 'replace' radio button... 65 | */ 66 | if (me.prop('value') === 'replace') { 67 | /* 68 | * Enable the extra replacement actions. 69 | */ 70 | parent.parent().find('.extra-actions').addClass('active').find('.disabler').hide(); 71 | /* 72 | * Otherwise disable the replacement actions. 73 | */ 74 | } else { 75 | parent.parent().find('.extra-actions').removeClass('active').find('.disabler').show(); 76 | } 77 | } 78 | }); 79 | 80 | 81 | }); 82 | -------------------------------------------------------------------------------- /priv/admin/js/nodes.js: -------------------------------------------------------------------------------- 1 | minispade.register('nodes', function() { 2 | 3 | /** 4 | * @class 5 | * 6 | * NodesController is responsible for displaying the list of nodes 7 | * in the cluster. 8 | */ 9 | RiakControl.NodesController = Ember.Controller.extend( 10 | /** 11 | * Shares properties with RiakControl.ClusterController 12 | */ 13 | RiakControl.ClusterAndNodeControls, 14 | /** @scope RiakControl.NodesController.prototype */ { 15 | 16 | /** 17 | * Reloads the record array associated with this controller. 18 | * 19 | * @returns {void} 20 | */ 21 | reload: function() { 22 | this.get('content').reload(); 23 | }, 24 | 25 | actions: { 26 | /** 27 | * Removes all checks from radio buttons. 28 | * 29 | * @returns {void} 30 | */ 31 | clearChecked: function() { 32 | $('#node-list input[type=radio]').each(function(index, item) { 33 | item.checked = false; 34 | $(item).parent().css('background-position', 'left top'); 35 | }); 36 | }, 37 | 38 | /** 39 | * Submits requests to stop and/or down nodes to the app. 40 | */ 41 | applyChanges: function() { 42 | var self = this; 43 | 44 | $("#node-list input[type='radio']:checked").each(function(index, item) { 45 | var name = item.name, 46 | action = item.value, 47 | replacement; 48 | 49 | // Empty string instead of undefined for null. 50 | if(replacement === undefined) { 51 | replacement = ''; 52 | } 53 | 54 | self.send('stageChange', name, action, replacement); 55 | }); 56 | 57 | self.send('clearChecked'); 58 | } 59 | } 60 | 61 | }); 62 | 63 | /** 64 | * @class 65 | * 66 | * One item in the collection of current cluster views. 67 | */ 68 | RiakControl.CurrentNodesItemView = Ember.View.extend( 69 | /** 70 | * Shares properties with other views that display lists of nodes. 71 | */ 72 | RiakControl.NodeProperties, 73 | /** @scope RiakControl.CurrentNodesItemView.prototype */ { 74 | 75 | /* Bindings from the model */ 76 | 77 | templateName: 'current_nodes_item', 78 | nameBinding: 'content.name', 79 | reachableBinding: 'content.reachable', 80 | statusBinding: 'content.status', 81 | ring_pctBinding: 'content.ring_pct', 82 | pending_pctBinding: 'content.pending_pct', 83 | mem_totalBinding: 'content.mem_total', 84 | mem_usedBinding: 'content.mem_used', 85 | mem_erlangBinding: 'content.mem_erlang', 86 | meBinding: 'content.me', 87 | 88 | /** 89 | * In order for labels to be clickable, they need to be bound to checks/radios 90 | * by ID. However, since these nodes are cloned by Ember, we need a way to make 91 | * sure all of those elements get id's that don't override each other. This 92 | * function gives us an ID string we can use as a prefix for id's on these other 93 | * elements. 94 | * 95 | * @returns {String} 96 | */ 97 | nodeID: function() { 98 | return Ember.guidFor(this); 99 | }.property(), 100 | 101 | /** 102 | * An ID value for the leave normally radio button and corresponding label. 103 | */ 104 | stopRadio: function() { 105 | return this.get('nodeID') + '_stop_node'; 106 | }.property('nodeID'), 107 | 108 | /** 109 | * An ID value for the force leave radio button and corresponding label. 110 | */ 111 | downRadio: function() { 112 | return this.get('nodeID') + '_down_node'; 113 | }.property('nodeID'), 114 | 115 | /** 116 | * A node can not be stopped when: 117 | * - It is unreachable. 118 | * - It is down. 119 | */ 120 | stopRadioClasses: function() { 121 | var status = this.get('status'), 122 | reachable = this.get('reachable'), 123 | classes = 'gui-radio-wrapper'; 124 | if (!reachable || status === 'down') { 125 | classes += ' semi-transparent'; 126 | } 127 | return classes; 128 | }.property('status', 'reachable'), 129 | 130 | /** 131 | * A node can not be marked as down when: 132 | * - It is alive and well 133 | * - It is already down. 134 | */ 135 | downRadioClasses: function() { 136 | var status = this.get('status'), 137 | reachable = this.get('reachable'), 138 | classes = 'gui-radio-wrapper'; 139 | if ((reachable && status === 'valid') || status === 'down') { 140 | classes += ' semi-transparent'; 141 | } 142 | return classes; 143 | }.property('status', 'reachable'), 144 | 145 | /** 146 | * When a node can't be stopped, disable the user 147 | * from clicking the stop radio button. 148 | */ 149 | stopDisablerClasses: function() { 150 | return 'disabler' + (/\ssemi\-transparent$/.test(this.get('stopRadioClasses')) ? ' show' : ''); 151 | }.property('stopRadioClasses'), 152 | 153 | /** 154 | * When a node can't be downed, disable the user from 155 | * clicking the down radio button. 156 | */ 157 | downDisablerClasses: function() { 158 | return 'disabler' + (/\ssemi\-transparent$/.test(this.get('downRadioClasses')) ? ' show' : ''); 159 | }.property('downRadioClasses') 160 | }); 161 | 162 | /** 163 | * @class 164 | * 165 | * Collection view for showing the current cluster. 166 | */ 167 | RiakControl.CurrentNodesView = Ember.CollectionView.extend( 168 | /** @scope RiakControl.CurrentClusterView.prototype */ { 169 | itemViewClass: RiakControl.CurrentNodesItemView 170 | }); 171 | 172 | }); 173 | -------------------------------------------------------------------------------- /priv/admin/js/router.js: -------------------------------------------------------------------------------- 1 | minispade.register('router', function() { 2 | 3 | /** 4 | * @class 5 | * 6 | * Base application router for the RiakControl application. At this 7 | * point its responsibilities include either rendering the new pure 8 | * ember-only components, or using the legacy pub/sub implementation 9 | * to render older pages which are scheduled for deprecation. 10 | */ 11 | RiakControl.Router.map(function() { 12 | /** @scope Ember.RouterDSL callback */ 13 | 14 | this.route('snapshot'); 15 | this.route('cluster'); 16 | this.route('nodes'); 17 | this.route('ring'); 18 | }); 19 | 20 | RiakControl.IndexRoute = Ember.Route.extend( 21 | /** @scope Ember.Route.prototype */ { 22 | 23 | redirect: function() { 24 | this.transitionTo('snapshot'); 25 | } 26 | }); 27 | 28 | RiakControl.SnapshotRoute = Ember.Route.extend( 29 | /** @scope Ember.Route.prototype */ { 30 | 31 | model: function() { 32 | return this.store.find('node'); 33 | }, 34 | 35 | renderTemplate: function() { 36 | this.render('snapshot'); 37 | $.riakControl.markNavActive('nav-snapshot'); 38 | }, 39 | 40 | activate: function() { 41 | this.controllerFor('snapshot').startInterval(); 42 | }, 43 | 44 | deactivate: function() { 45 | this.controllerFor('snapshot').cancelInterval(); 46 | } 47 | }); 48 | 49 | RiakControl.ClusterRoute = Ember.Route.extend( 50 | /** @scope Ember.Route.prototype */ { 51 | 52 | model: function() { 53 | var cluster = this.controllerFor('cluster'); 54 | 55 | /* 56 | * Don't return a promise if we have previously 57 | * failed to load data. 58 | */ 59 | if (cluster.get('cannotLoad') === true) { 60 | return cluster.get('content'); 61 | 62 | } else { 63 | return cluster.load().then(function (d) { 64 | return d; 65 | }); 66 | } 67 | }, 68 | 69 | /* 70 | * In the event of an error, the cannotLoad property 71 | * will be set to true. This way we can still 72 | * transition into the state without getting caught 73 | * in an infinit .load() loop. 74 | */ 75 | events: { 76 | error: function () { 77 | this.transitionTo('cluster'); 78 | } 79 | }, 80 | 81 | renderTemplate: function() { 82 | this.render('cluster'); 83 | $.riakControl.markNavActive('nav-cluster'); 84 | }, 85 | 86 | activate: function() { 87 | this.controllerFor('cluster').startInterval(); 88 | }, 89 | 90 | deactivate: function() { 91 | this.controllerFor('cluster').cancelInterval(); 92 | } 93 | }); 94 | 95 | RiakControl.NodesRoute = Ember.Route.extend( 96 | /** @scope Ember.Route.prototype */ { 97 | 98 | model: function() { 99 | return this.store.find('node'); 100 | }, 101 | 102 | renderTemplate: function() { 103 | this.render('nodes'); 104 | $.riakControl.markNavActive('nav-nodes'); 105 | }, 106 | 107 | activate: function() { 108 | this.controllerFor('nodes').startInterval(); 109 | }, 110 | 111 | deactivate: function() { 112 | this.controllerFor('nodes').cancelInterval(); 113 | } 114 | }); 115 | 116 | RiakControl.RingRoute = Ember.Route.extend( 117 | /** @scope Ember.Route.prototype */ { 118 | 119 | model: function() { 120 | var ring = this.controllerFor('ring'); 121 | 122 | /* 123 | * Don't return a promise if we have previously 124 | * failed to load data. 125 | */ 126 | if (ring.get('cannotLoad') === true) { 127 | return ring.get('content'); 128 | 129 | } else { 130 | return ring.load().then(function (d) { 131 | return d; 132 | }); 133 | } 134 | }, 135 | 136 | /* 137 | * In the event of an error, the cannotLoad property 138 | * will be set to true. This way we can still 139 | * transition into the state without getting caught 140 | * in an infinit .load() loop. 141 | */ 142 | events: { 143 | error: function () { 144 | this.transitionTo('ring'); 145 | } 146 | }, 147 | 148 | renderTemplate: function() { 149 | this.render('ring'); 150 | $.riakControl.markNavActive('nav-ring'); 151 | }, 152 | 153 | activate: function() { 154 | this.controllerFor('ring').startInterval(); 155 | }, 156 | 157 | deactivate: function() { 158 | this.controllerFor('ring').cancelInterval(); 159 | } 160 | }); 161 | 162 | RiakControl.LoadingRoute = Ember.Route.extend({}); 163 | }); 164 | -------------------------------------------------------------------------------- /priv/admin/js/shared.js: -------------------------------------------------------------------------------- 1 | minispade.register('shared', function () { 2 | 3 | /** 4 | * ClusterAndNodeControls contains properties shared by: 5 | * - RiakControl.ClusterController 6 | * - RiakControl.NodesController 7 | * - RiakControl.RingController 8 | */ 9 | RiakControl.ClusterAndNodeControls = Ember.Mixin.create({ 10 | 11 | actions: { 12 | /** 13 | * Stage a change. 14 | * 15 | * @returns {void} 16 | */ 17 | stageChange: function(node, action, replacement, success, failure) { 18 | var self = this, 19 | ajax = $.ajax({ 20 | type: 'PUT', 21 | url: '/admin/cluster', 22 | dataType: 'json', 23 | data: { changes: 24 | [{ 25 | node: node, 26 | action: action, 27 | replacement: replacement 28 | }] 29 | } 30 | }); 31 | 32 | ajax.then( 33 | 34 | // success... 35 | function(d) { 36 | if(success) { 37 | success(); 38 | } 39 | 40 | self.reload(); 41 | }, 42 | 43 | // error... 44 | function (jqXHR, textStatus, errorThrown) { 45 | if(failure) { 46 | failure(); 47 | } 48 | 49 | self.get('displayError').call(self, jqXHR, textStatus, errorThrown); 50 | } 51 | ); 52 | 53 | }, 54 | 55 | /** 56 | * The action specified on the tag creating the 'x' button in the error message div. 57 | * By setting the 'errorMessage' property back to an empty string, the message will disappear. 58 | * 59 | * @returns {void} 60 | */ 61 | hideError: function () { 62 | this.set('errorMessage', ''); 63 | } 64 | }, 65 | 66 | /** 67 | * If content is loading, return true. 68 | * 69 | * @returns {boolean} 70 | */ 71 | isLoading: false, 72 | 73 | /** 74 | * Holds the most recent error message. 75 | */ 76 | errorMessage: '', 77 | 78 | /** 79 | * Whenever an ajax call returns an error, we display 80 | * the error for the user. 81 | * 82 | * @param {Object} jqXHR The xhr request object generated by jQuery. 83 | * @param {String} textStatus The status of the response. 84 | * @param {Error} errorThrown The error object produced by the ajax request. 85 | * 86 | * @returns {void} 87 | */ 88 | displayError: function (jqXHR, textStatus, errorThrown) { 89 | var parsed, errors; 90 | 91 | if(jqXHR) { 92 | try { 93 | parsed = JSON.parse(jqXHR.responseText); 94 | errors = parsed.errors.join(', '); 95 | } catch(err) { 96 | errors = errorThrown; 97 | } 98 | } else { 99 | errors = errorThrown; 100 | } 101 | 102 | this.set('errorMessage', 'Request failed: ' + errors); 103 | }, 104 | 105 | /** 106 | * Called by the router, to start polling when this controller/view is navigated to. 107 | * 108 | * @returns {void} 109 | */ 110 | startInterval: function() { 111 | this._intervalId = setInterval($.proxy(this.reload, this), RiakControl.refreshInterval); 112 | }, 113 | 114 | /** 115 | * Called by the router, to stop polling when this controller/view is navigated away from. 116 | * 117 | * @returns {void} 118 | */ 119 | cancelInterval: function() { 120 | if(this._intervalId) { 121 | clearInterval(this._intervalId); 122 | } 123 | } 124 | }); 125 | 126 | /** 127 | * RiakControl.NodeProperties contains properties shared by: 128 | * - RiakControl.CurrentClusterItemView 129 | * - RiakControl.StagedClusterItemView 130 | * - RiakControl.CurrentNodesItemView 131 | */ 132 | RiakControl.NodeProperties = Ember.Mixin.create({ 133 | 134 | /** 135 | * In the current cluster area, the host node has extra content in its 136 | * actions box so it needs to be a little taller than the others. 137 | * 138 | * @returns {String} 139 | */ 140 | hostNodeClasses: function () { 141 | return this.get('me') ? 'node taller' : 'node'; 142 | }.property('me'), 143 | 144 | /** 145 | * Color the lights appropriately based on the node status. 146 | * 147 | * @returns {string} 148 | */ 149 | indicatorLights: function() { 150 | var status = this.get('status'); 151 | var reachable = this.get('reachable'); 152 | var color; 153 | 154 | if(reachable === false && status !== "down") { 155 | color = "red"; 156 | } else if (status === "down") { 157 | color = "blue"; 158 | } else if(status === 'leaving' || status === 'joining') { 159 | color = "orange"; 160 | } else if (status === 'valid') { 161 | color = "green"; 162 | } else { 163 | color = "grey"; 164 | } 165 | 166 | return "gui-light status-light inline-block " + color; 167 | }.property('reachable', 'status'), 168 | 169 | /** 170 | * Color the arrows in the partitions column appropriately based 171 | * on how ring_pct and pending_pct compare. 172 | * 173 | * @returns {String} 174 | */ 175 | coloredArrows: function() { 176 | var current = this.get('ring_pct'), 177 | pending = this.get('pending_pct'), 178 | common = 'left pct-arrows '; 179 | 180 | if (pending > current) { 181 | return common + 'pct-gaining'; 182 | } else if (pending < current) { 183 | return common + 'pct-losing'; 184 | } else { 185 | return common + 'pct-static'; 186 | } 187 | }.property('ring_pct', 'pending_pct'), 188 | 189 | /** 190 | * Normalizer. 191 | * 192 | * @returns {number} 193 | */ 194 | memDivider: function() { 195 | return this.get('mem_total') / 100; 196 | }.property('mem_total'), 197 | 198 | /** 199 | * Compute memory ceiling. 200 | * 201 | * @returns {number} 202 | */ 203 | memErlangCeil: function () { 204 | return Math.ceil(this.get('mem_erlang') / this.get('memDivider')); 205 | }.property('mem_erlang', 'memDivider'), 206 | 207 | /** 208 | * Compute free memory from total and used. 209 | * 210 | * @returns {number} 211 | */ 212 | memNonErlang: function () { 213 | return Math.round( 214 | (this.get('mem_used') / this.get('memDivider')) - this.get('memErlangCeil')); 215 | }.property('mem_used', 'memDivider', 'memErlangCeil'), 216 | 217 | /** 218 | * Compute free memory from total and used. 219 | * 220 | * @returns {number} 221 | */ 222 | memFree: function () { 223 | return this.get('mem_total') - this.get('mem_used'); 224 | }.property('mem_total', 'mem_used'), 225 | 226 | /** 227 | * Format free memory to be a readbale version. 228 | * 229 | * @returns {number} 230 | */ 231 | memFreeReadable: function () { 232 | return Math.round(this.get('memFree') / this.get('memDivider')); 233 | }.property('memFree', 'memDivider'), 234 | 235 | /** 236 | * Format used memory to be a readbale version. 237 | * 238 | * @returns {number} 239 | */ 240 | memUsedReadable: function () { 241 | return Math.round((this.get('mem_total') - this.get('memFree')) / 242 | this.get('memDivider')); 243 | }.property('mem_total', 'memFree', 'memDivider'), 244 | 245 | /** 246 | * Return CSS style for rendering memory used by Erlang. 247 | * 248 | * @returns {number} 249 | */ 250 | memErlangStyle: function () { 251 | return 'width: ' + this.get('memErlangCeil') + '%'; 252 | }.property('memErlangCeil'), 253 | 254 | /** 255 | * Return CSS style for rendering occupied non-erlang memory. 256 | * 257 | * @returns {string} 258 | */ 259 | memNonErlangStyle: function () { 260 | return 'width: ' + this.get('memNonErlang') + '%'; 261 | }.property('memNonErlang'), 262 | 263 | /** 264 | * Return CSS style for rendering free memory. 265 | * 266 | * @returns {string} 267 | */ 268 | memFreeStyle: function () { 269 | return 'width: ' + this.get('memFreeReadable') + '%'; 270 | }.property('memFreeReadable'), 271 | 272 | /** 273 | * Formatted ring percentage. 274 | * 275 | * @returns {string} 276 | */ 277 | ringPctReadable: function () { 278 | return Math.round(this.get('ring_pct') * 100); 279 | }.property('ring_pct') 280 | }); 281 | 282 | }); 283 | -------------------------------------------------------------------------------- /priv/admin/js/snapshot.js: -------------------------------------------------------------------------------- 1 | minispade.register('snapshot', function() { 2 | 3 | /** 4 | * @class 5 | * 6 | * SnapshotController is responsible for rendering of the snapshot page and 7 | * deriving overall cluster state from a selection of nodes. 8 | */ 9 | RiakControl.SnapshotController = Ember.ObjectController.extend( 10 | /** @scope RiakControl.SnapshotController.prototype */ { 11 | 12 | /** 13 | * Reloads the record array associated with this controller. 14 | * 15 | * @returns {void} 16 | */ 17 | reload: function() { 18 | this.get('content').reload(); 19 | }, 20 | 21 | /** 22 | * Called by the router, to start polling when this controller/view is navigated to. 23 | * 24 | * @returns {void} 25 | */ 26 | startInterval: function() { 27 | this._intervalId = setInterval($.proxy(this.reload, this), RiakControl.refreshInterval); 28 | }, 29 | 30 | /** 31 | * Called by the router, to stop polling when this controller/view is navigated away from. 32 | * 33 | * @returns {void} 34 | */ 35 | cancelInterval: function() { 36 | if(this._intervalId) { 37 | clearInterval(this._intervalId); 38 | } 39 | }, 40 | 41 | /** 42 | * Filter nodes and return just nodes classified as unreachable. 43 | * 44 | * @returns {Array} filtered list of nodes 45 | */ 46 | unreachableNodes: function() { 47 | return this.get('content').filterProperty('reachable', false); 48 | }.property('content.@each.reachable'), 49 | 50 | /** 51 | * Determine if there are any unreachable nodes. 52 | * 53 | * @returns {Boolean} 54 | */ 55 | areUnreachableNodes: function() { 56 | return this.get('unreachableNodes.length') > 0; 57 | }.property('content.@each.reachable'), 58 | 59 | /** 60 | * Filter nodes and return just nodes classified as incompatible. 61 | * 62 | * @returns {Array} filtered list of nodes 63 | */ 64 | incompatibleNodes: function() { 65 | return this.get('content').filterProperty('status', 'incompatible'); 66 | }.property('content.@each.status'), 67 | 68 | /** 69 | * Determine if there are any incompatible nodes. 70 | * 71 | * @returns {Boolean} 72 | */ 73 | areIncompatibleNodes: function() { 74 | return this.get('incompatibleNodes.length') > 0; 75 | }.property('content.@each.status'), 76 | 77 | /** 78 | * Filter nodes and return just nodes classified as down. 79 | * 80 | * @returns {Array} filtered list of nodes 81 | */ 82 | downNodes: function() { 83 | return this.get('content').filterProperty('status', 'down'); 84 | }.property('content.@each.status'), 85 | 86 | /** 87 | * Determine if there are any down nodes. 88 | * 89 | * @returns {Boolean} 90 | */ 91 | areDownNodes: function() { 92 | return this.get('downNodes.length') > 0; 93 | }.property('content.@each.status'), 94 | 95 | /** 96 | * Filter nodes and return just nodes classified as low-memory nodes. 97 | * 98 | * @returns {Array} filtered list of nodes 99 | */ 100 | lowMemNodes: function() { 101 | return this.get('content').filterProperty('low_mem', true); 102 | }.property('content.@each.low_mem'), 103 | 104 | /** 105 | * Determine if there are any low-memory nodes. 106 | * 107 | * @returns {Boolean} 108 | */ 109 | areLowMemNodes: function() { 110 | return this.get('lowMemNodes.length') > 0; 111 | }.property('content.@each.low_mem'), 112 | 113 | /** 114 | * Derive overall state of the cluster based on node status. 115 | * 116 | * @return {Boolean} whether the cluster is happy or not 117 | */ 118 | healthyCluster: function() { 119 | var areUnreachableNodes = this.get('areUnreachableNodes'); 120 | var areIncompatibleNodes = this.get('areIncompatibleNodes'); 121 | var areDownNodes = this.get('areDownNodes'); 122 | var areLowMemNodes = this.get('areLowMemNodes'); 123 | 124 | return !(areUnreachableNodes || areIncompatibleNodes || areDownNodes || areLowMemNodes); 125 | }.property('areUnreachableNodes', 'areIncompatibleNodes', 'areDownNodes', 'areLowMemNodes') 126 | }); 127 | 128 | }); 129 | -------------------------------------------------------------------------------- /priv/admin/js/templates/all_unavailable_chart.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{view.normalizedAbnormal}}% 4 | of partitions have all primary replicas on down or unreachable nodes. 5 | 6 |
7 | 8 | DETAILS 9 | 10 |
11 |
12 |

13 | Preference lists where all primary replicas are unavailable. 14 |

15 | {{#if allUnavailableExist}} 16 | {{allUnavailableCount}} partitions have all of their 17 | primary replicas located on down or unreachable nodes. It is 18 | recommended that you work to restore service to the unreachable nodes. 19 | {{else}} 20 | No partitions have all of their primary replicas located on down or 21 | unreachable nodes. There is nothing to worry about. 22 | {{/if}} 23 |
24 |
25 | -------------------------------------------------------------------------------- /priv/admin/js/templates/application.hbs: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 | 17 |
{{outlet}}
18 | 19 |
31 | -------------------------------------------------------------------------------- /priv/admin/js/templates/cluster.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 | 7 |

Cluster Management

8 | 9 |
10 |
11 |
12 |
13 | 14 |
15 | {{#if standalone}} 16 |

Join Node

17 | Type the name of a node in an existing cluster to join this node to. 18 | {{else}} 19 |

Add Node

20 | Type the name of a node to add to this cluster. 21 | {{/if}} 22 | 23 | 24 | 27 | 36 | 37 |
25 | {{view RiakControl.JoinNodeView valueBinding="controller.joinNodeField"}} 26 | 28 | 29 | {{#if standalone}} 30 | JOIN NODE 31 | {{else}} 32 | ADD NODE 33 | {{/if}} 34 | 35 |
38 | {{#if errorMessage}} 39 |
40 | 41 | {{errorMessage}} 42 |
43 | {{/if}} 44 |
45 | 46 | 47 |
48 |

49 | Current Cluster 50 |
51 |

52 |
53 | {{#if controller.isLoading}} 54 |
55 | 56 |

Loading...

57 |
58 | {{else}} 59 |
    60 |
  • Actions

  • 61 |
  • Name & Status

  • 62 |
  • Partitions

  • 63 |
  • RAM Usage

  • 64 |
65 |
66 | {{collection RiakControl.CurrentClusterView contentBinding="activeCurrentCluster"}} 67 | {{/if}} 68 |
69 |
70 | 71 | 72 |
73 |
74 |
75 | 76 | 77 |
78 |

79 | Staged Changes 80 | (Your new cluster after convergence.) 81 |

82 | 83 | {{#if controller.displayPlan}} 84 |
85 |
    86 |
  • Name & Status

  • 87 |
  • Partitions

  • 88 |
  • Action

  • 89 |
  • Replacement

  • 90 |
91 |
92 | {{collection RiakControl.StagedClusterView contentBinding="activeStagedCluster"}} 93 |
94 |
95 |
96 | 97 | 98 |
99 | 100 | COMMIT 101 | 102 |
103 |
104 | 105 | Changed your mind? Click this button to remove all staged changes. 106 | 107 | 108 | CLEAR PLAN 109 | 110 |
111 | {{else}} 112 |
113 |
114 | {{#if controller.ringNotReady}} 115 |

116 | Please wait, the ring is converging. 117 |

118 | {{else}} 119 | {{#if controller.legacyRing}} 120 |

You are currently running a legacy version of Riak that does not support staged changes.

121 | {{else}} 122 | {{#if controller.emptyPlan}} 123 |

Currently no staged changes to display.

124 | {{else}} 125 | {{#if controller.isLoading}} 126 | 127 |

Loading...

128 | {{/if}} 129 | {{/if}} 130 | {{/if}} 131 | {{/if}} 132 |
133 |
134 | {{/if}} 135 |
136 | 137 |
138 | 139 |
140 | -------------------------------------------------------------------------------- /priv/admin/js/templates/current_cluster_item.hbs: -------------------------------------------------------------------------------- 1 | {{#with view}} 2 |
3 |
4 | {{#view RiakControl.CurrentClusterToggleView}} 5 |
6 | 7 |
8 | {{/view}} 9 |
10 |
11 |
12 |
13 |
{{name}}
14 |
15 |
16 |
17 |
18 |
19 | {{ringPctReadable}}% 20 |
21 |
22 |
23 |
24 | {{#if reachable}} 25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {{memUsedReadable}}% 34 | {{else}} 35 |
36 |
37 |
38 |
39 |
40 |
41 | 42 | {{/if}} 43 |
44 |
45 | 46 | 47 |
48 |
49 |
50 |

51 | Use these actions to prepare this node to leave the cluster. 52 |

53 | {{#if me}} 54 | 55 | Warning: This node is hosting Riak Control. If it leaves the 56 | cluster, Riak Control will be shut down. 57 | 58 | {{/if}} 59 |
60 |
61 | 62 | 63 |
64 |
65 | 66 | 67 |
68 |
69 | 70 | 71 |
72 |
73 |
74 | {{#if controller.joiningNodesExist}} 75 |
76 |
Select Replacement Node
77 |
78 | {{view RiakControl.ClusterItemSelectView 79 | prompt="Select Replacement Node" 80 | classNames="gui-dropdown" 81 | contentBinding="controller.joiningNodes" 82 | optionLabelPath="content.name" 83 | optionValuePath="content.name"}} 84 |
85 |
86 | 87 | 88 |
89 |
90 |
91 | {{else}} 92 |
93 | No new nodes are currently staged to join. 94 |
95 |
96 | {{/if}} 97 |
98 |
99 |
100 |
101 | Click "STAGE" when you are ready to stage this action. 102 | 103 | STAGE 104 | 105 |
106 |
107 |
108 |
109 |
110 |
111 | 112 | {{/with}} 113 | -------------------------------------------------------------------------------- /priv/admin/js/templates/current_nodes_item.hbs: -------------------------------------------------------------------------------- 1 | {{#with view}} 2 |
3 |
4 |
5 | 6 |
7 |
8 |
9 |
10 |
11 | 12 |
13 |
14 |
15 |
16 |
17 |
18 |
{{name}}
19 |
20 |
21 |
22 |
23 |
24 | {{ringPctReadable}}% 25 |
26 |
27 |
28 |
29 | {{#if reachable}} 30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | {{memUsedReadable}}% 39 | {{else}} 40 |
41 |
42 |
43 |
44 |
45 |
46 | 47 | {{/if}} 48 |
49 |
50 |
51 | 52 | {{/with}} 53 | -------------------------------------------------------------------------------- /priv/admin/js/templates/degenerate_preflist_chart.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{view.normalizedAbnormal}}% 4 | of partitions have primary replicas that do not live on a unique set 5 | of nodes. 6 | 7 |
8 | 9 | DETAILS 10 | 11 |
12 |
13 |

14 | Preference lists which do not contain replicas on distinct nodes. 15 |

16 | {{#if degeneratesExist}} 17 | {{degenerateCount}} partitions have multiple replicas 18 | located on the same node. It is highly recommended that you add 20 | nodes to resolve this and ensure the high availability and fault 21 | tolerance of your data. 22 | {{else}} 23 | All partitions have replicas on distinct nodes. There is nothing to 24 | worry about. 25 | {{/if}} 26 |
27 |
28 | -------------------------------------------------------------------------------- /priv/admin/js/templates/loading.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 | 7 |

...

8 | 9 |
10 |
11 |
12 |
13 | 14 |
15 | 16 |

Loading...

17 |
18 | 19 |
-------------------------------------------------------------------------------- /priv/admin/js/templates/nodes.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 | 7 |

Node Management

8 | 9 |
10 |
11 |
12 |
13 | 14 | {{#if errorMessage}} 15 |
16 | 17 | {{errorMessage}} 18 |
19 | {{/if}} 20 | 21 |
22 |

23 | Current Cluster
24 |

25 | 26 | Click the radio button for each node you would like to stop or mark as down, then click "APPLY" to apply your changes. If the radio button is grayed out, the action is not 27 | available due to the current status of the node. 28 |
29 |
30 | {{#if controller.isLoading}} 31 |
32 | 33 |

Loading...

34 |
35 | {{else}} 36 |
    37 |
  • Stop

  • 38 |
  • Down

  • 39 |
  • Name & Status

  • 40 |
  • Partitions

  • 41 |
  • RAM Usage

  • 42 |
43 |
44 | {{collection RiakControl.CurrentNodesView contentBinding="content"}} 45 | {{/if}} 46 |
47 |
48 | 49 | APPLY 50 | 51 | 52 | CLEAR 53 | 54 |
55 |
56 |
57 | 58 |
59 | -------------------------------------------------------------------------------- /priv/admin/js/templates/pagination_item.hbs: -------------------------------------------------------------------------------- 1 | {{#with view}} 2 | 3 | {{content.page_id}} 4 | 5 | {{/with}} 6 | -------------------------------------------------------------------------------- /priv/admin/js/templates/partition.hbs: -------------------------------------------------------------------------------- 1 | {{#with view}} 2 |
3 | {{/with}} 4 | -------------------------------------------------------------------------------- /priv/admin/js/templates/quorum_unavailable_chart.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{view.normalizedAbnormal}}% 4 | of partitions have a majority of primary replicas down or unreachable 5 | nodes. 6 | 7 |
8 | 9 | DETAILS 10 | 11 |
12 |
13 |

14 | Preference lists where a majority of primary replicas are unavailable. 15 |

16 | {{#if quorumUnavailableExist}} 17 | {{quorumUnavailableCount}} partitions have a majority 18 | of their replicas located on down or unreachable nodes. It is 19 | recommended that you work to restore service to the unreachable nodes. 20 | {{else}} 21 | No partitions have a majority of their replicas located on down or 22 | unreachable nodes. There is nothing to worry about. 23 | {{/if}} 24 |
25 |
26 | -------------------------------------------------------------------------------- /priv/admin/js/templates/ring.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 | 7 |

Ring Status

8 | 9 |
10 |
11 |
12 |
13 | 14 | {{#if errorMessage}} 15 |
16 | 17 | {{errorMessage}} 18 |
19 | {{/if}} 20 | 21 |
22 | 23 |
24 |

Availability

25 | 26 |
27 |

See visualizations using a different n_val.

28 |
29 | 30 | 31 | n_val specifies the number of copies of each object to be stored 32 | in the cluster. 33 | 34 | 35 | 36 | Right now, you\'re seeing what your ring looks like 37 | for all objects stored with an n_val of 3. If you have 38 | objects stored with other n_vals, select an n_val 39 | from the dropdown to see what your ring looks like 40 | for data stored with the selected n_val. 41 | 42 | 43 |
44 | {{view RiakControl.NValSelectView 45 | contentBinding="content.n_vals" 46 | valueBinding="content.selected" 47 | dataBinding="content" 48 | selectionBinding="content.selected" 49 | }} 50 |
51 | 52 | {{view RiakControl.DegeneratePreflistChart}} 53 |
54 | 55 | {{view RiakControl.QuorumUnavailableChart}} 56 |
57 | 58 | {{view RiakControl.AllUnavailableChart}} 59 | 60 |
61 |
62 | 63 |
64 |

Partitions

65 | 66 |
67 |

Partitions

68 | {{collection RiakControl.PartitionsView contentBinding="content"}} 69 |
70 |
71 | 72 |
73 |
74 |

Partition Information

75 |
76 | {{#if partitionSelected}} 77 | 78 |
79 |

Index

80 | 81 | {{selectedPartition.index}} 82 | 83 |
84 | 85 | {{#if selectedPartition.unavailable}} 86 |
87 |

Unavailable Primaries

88 |
    89 | {{#each selectedPartition.unavailable_nodes}} 90 |
  • {{this}}
  • 91 | {{/each}} 92 |
93 |
94 | {{/if}} 95 | 96 |
97 | {{#if selectedPartition.all_nodes.length}} 98 |

All Primaries

99 |
    100 | {{#each selectedPartition.all_nodes}} 101 |
  • {{this}}
  • 102 | {{/each}} 103 |
104 | {{else}} 105 |

No Primaries To Display

106 | {{/if}} 107 |
108 | 109 | {{else}} 110 | 111 | Click on a partition square to view its more information. 112 | 113 | {{/if}} 114 |
115 |
116 | 117 |

Legend

118 |
119 | Only fallbacks available.
120 |
121 | Majority of primary replicas down.
122 |
123 | Replicas do not live on unique nodes.
124 |
125 | Available.
126 |
127 | 128 |
129 |
130 |
131 |
132 | 133 |
134 | -------------------------------------------------------------------------------- /priv/admin/js/templates/snapshot.hbs: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 |
6 |
7 | 8 |

Current Snapshot

9 | 10 |
11 |
12 |
13 |
14 |
15 | {{#if healthyCluster}} 16 |
17 | 18 |
19 |

Your cluster is healthy.

20 |

You currently have...

21 |
    22 |
  • 0 Unreachable nodes
  • 23 |
  • 0 Incompatible nodes
  • 24 |
  • 0 Nodes marked as down
  • 25 |
  • 0 Nodes experiencing low memory
  • 26 |
  • Nothing to worry about because Riak is your friend
  • 27 |
28 |
29 |
30 | {{else}} 31 |
32 | 33 |
34 |

Your cluster has problems.

35 | 36 | {{#if areUnreachableNodes}} 37 | 38 |

The following nodes are currently unreachable:

39 |
    40 | {{#each unreachableNodes}} 41 |
  • {{name}}
  • 42 | {{/each}} 43 |
44 | {{/if}} 45 | 46 | {{#if areIncompatibleNodes}} 47 | 48 |

Some 49 | information about the following nodes may be temporarily 50 | unavailable:

51 |
    52 | {{#each incompatibleNodes}} 53 |
  • {{name}}
  • 54 | {{/each}} 55 |
56 |

This may be triggered by using control in the middle 57 | of a rolling upgrade or during startup of the node.

58 | {{/if}} 59 | 60 | {{#if areDownNodes}} 61 | 62 |

The following nodes are currently marked down:

63 |
    64 | {{#each downNodes}} 65 |
  • {{name}}
  • 66 | {{/each}} 67 |
68 | {{/if}} 69 | 70 | {{#if areLowMemNodes}} 71 | 72 |

The following nodes are currently experiencing low memory:

73 |
    74 | {{#each lowMemNodes}} 75 |
  • {{name}}
  • 76 | {{/each}} 77 |
78 | {{/if}} 79 |
80 |
81 | {{/if}} 82 |
83 | 84 |
85 | -------------------------------------------------------------------------------- /priv/admin/js/templates/staged_cluster_item.hbs: -------------------------------------------------------------------------------- 1 | {{#with view}} 2 |
3 |
4 |
5 |
6 |
{{name}}
7 |
8 |
9 |
10 |
11 | {{ringPctReadable}}% 12 |
13 |
14 |
15 | {{#if isAction}} 16 |
17 | {{node_action}} 18 |
19 | {{/if}} 20 | {{#if isReplaced}} 21 |
22 |
23 |
{{content.replacement}}
24 |
25 |
26 | {{/if}} 27 |
28 |
29 | {{/with}} 30 | -------------------------------------------------------------------------------- /priv/admin/js/vendor/minispade.js: -------------------------------------------------------------------------------- 1 | if (typeof document !== "undefined") { 2 | (function() { 3 | minispade = { 4 | root: null, 5 | modules: {}, 6 | loaded: {}, 7 | 8 | globalEval: function(data) { 9 | if ( data ) { 10 | var ev = "ev"; 11 | var execScript = "execScript"; 12 | 13 | // We use execScript on Internet Explorer 14 | // We use an anonymous function so that context is window 15 | // rather than jQuery in Firefox 16 | ( window[execScript] || function( data ) { 17 | window[ ev+"al" ].call( window, data ); 18 | } )( data ); 19 | } 20 | }, 21 | 22 | require: function(name) { 23 | var loaded = minispade.loaded[name]; 24 | var mod = minispade.modules[name]; 25 | 26 | if (!loaded) { 27 | if (mod) { 28 | minispade.loaded[name] = true; 29 | 30 | if (typeof mod === "string") { 31 | this.globalEval(mod); 32 | } else { 33 | mod(); 34 | } 35 | } else { 36 | if (minispade.root && name.substr(0,minispade.root.length) !== minispade.root) { 37 | return minispade.require(minispade.root+name); 38 | } else { 39 | throw "The module '" + name + "' could not be found"; 40 | } 41 | } 42 | } 43 | 44 | return loaded; 45 | }, 46 | 47 | register: function(name, callback) { 48 | minispade.modules[name] = callback; 49 | } 50 | }; 51 | })(); 52 | } 53 | -------------------------------------------------------------------------------- /priv/riak_control.schema: -------------------------------------------------------------------------------- 1 | %%-*- mode: erlang -*- 2 | %% riak_control config 3 | %% @doc Set to 'off' to disable the admin panel. 4 | {mapping, "riak_control", "riak_control.enabled", [ 5 | {default, off}, 6 | {datatype, flag} 7 | ]}. 8 | 9 | %% @doc Authentication mode used for access to the admin panel. 10 | {mapping, "riak_control.auth.mode", "riak_control.auth", [ 11 | {default, off}, 12 | {datatype, {enum, [off, userlist]}} 13 | ]}. 14 | 15 | {translation, 16 | "riak_control.auth", 17 | fun(Conf) -> 18 | case cuttlefish:conf_get("riak_control.auth.mode", Conf) of 19 | userlist -> userlist; 20 | off -> none; 21 | _ -> none 22 | end 23 | end}. 24 | 25 | %% @doc If riak control's authentication mode (riak_control.auth.mode) 26 | %% is set to 'userlist' then this is the list of usernames and 27 | %% passwords for access to the admin panel. 28 | %% To create users with given names, add entries of the format: 29 | %% riak_control.auth.user.USERNAME.password = PASSWORD 30 | %% replacing USERNAME with the desired username and PASSWORD with the 31 | %% desired password for that user. 32 | %% @see riak_control.auth.mode 33 | {mapping, "riak_control.auth.user.$username.password", "riak_control.userlist", [ 34 | {commented, "pass"}, 35 | {include_default, "admin"} 36 | ]}. 37 | 38 | {translation, 39 | "riak_control.userlist", 40 | fun(Conf) -> 41 | UserList = cuttlefish_variable:filter_by_prefix("riak_control.auth.user", Conf), 42 | Users = [ {Username, Password} || {[_, _, _, Username, _], Password} <- UserList ], 43 | case Users of 44 | [] -> 45 | cuttlefish:unset(); 46 | _ -> Users 47 | end 48 | end}. 49 | -------------------------------------------------------------------------------- /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basho/riak_control/72bbb7a6d4d594b4f6590f297a93f60236217fbe/rebar -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang -*- 2 | 3 | {erl_opts, [warnings_as_errors, {parse_transform, lager_transform}, debug_info]}. 4 | 5 | {edoc_opts, [{preprocess, true}]}. 6 | 7 | {erlydtl_opts, [ 8 | {compiler_options, [report, return, debug_info]}]}. 9 | 10 | {cover_enabled, true}. 11 | 12 | {xref_checks, []}. 13 | {xref_queries, [{"(XC - UC) || (XU - X - B - \"(cluster_info|dtrace)\" : Mod)", []}]}. 14 | 15 | {deps, [ 16 | {webmachine, "1.10.8", {git, "git://github.com/basho/webmachine.git", {tag, "1.10.8-basho1"}}}, 17 | {riak_core, ".*", {git, "https://github.com/basho/riak_core.git", {tag, "2.1.10"}}}, 18 | {erlydtl, ".*", {git, "https://github.com/basho/erlydtl.git", "d20b53f0"}} 19 | ]}. 20 | 21 | {plugin_dir, "plugins"}. 22 | 23 | {plugins, [rebar_js_handlebars_plugin, 24 | rebar_js_stylus_plugin, 25 | rebar_js_concatenator_plugin]}. 26 | 27 | {js_handlebars, [ 28 | {doc_root, "priv/admin/js/templates"}, 29 | {out_dir, "priv/admin/js"}, 30 | {target, "Ember.TEMPLATES"}, 31 | {compiler, "Ember.Handlebars.compile"}, 32 | {templates, [{"generated/templates.js", ["application.hbs", 33 | "snapshot.hbs", 34 | "cluster.hbs", 35 | "nodes.hbs", 36 | "ring.hbs", 37 | "current_cluster_item.hbs", 38 | "current_nodes_item.hbs", 39 | "staged_cluster_item.hbs", 40 | "partition.hbs", 41 | "degenerate_preflist_chart.hbs", 42 | "quorum_unavailable_chart.hbs", 43 | "all_unavailable_chart.hbs", 44 | "loading.hbs"]}]} 45 | ]}. 46 | 47 | {js_concatenator, [ 48 | {doc_root, "priv/admin/js/vendor"}, 49 | {out_dir, "priv/admin/js"}, 50 | {concatenations, [{"generated/vendor.js", ["jquery-1.10.2.js", 51 | "jquery-ui-1.8.16.custom.min.js", 52 | "handlebars-1.0.0.js", 53 | "ember-1.0.0.js", 54 | "ember-data-1.0.0-beta.1.js", 55 | "minispade.js", 56 | "d3.v3.min.js"], []}]} 57 | ]}. 58 | 59 | {js_stylus, [ 60 | {stylus_path, "/usr/local/bin/stylus"}, 61 | {doc_root, "priv/admin/css"}, 62 | {out_dir, "priv/admin/css/compiled"}, 63 | {stylesheets, ["style.styl"]} 64 | ]}. 65 | -------------------------------------------------------------------------------- /src/riak_control.app.src: -------------------------------------------------------------------------------- 1 | {application, riak_control, 2 | [ 3 | {description, "Riak Admin Interface"}, 4 | {vsn, git}, 5 | {registered, []}, 6 | {applications, [ 7 | kernel, 8 | stdlib, 9 | webmachine, 10 | riak_core 11 | ]}, 12 | {mod, { riak_control_app, []}}, 13 | {env, []} 14 | ]}. 15 | -------------------------------------------------------------------------------- /src/riak_control.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------ 2 | %% 3 | %% Copyright (c) 2011 Basho Technologies, Inc. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------ 20 | %% 21 | %% @doc Utilities needed by many pieces of the riak_control app. 22 | %% 23 | -module(riak_control). 24 | 25 | -export([is_app_up/1, 26 | priv_dir/0]). 27 | 28 | %% @doc Find out if any nodes are running a given Riak app. 29 | -spec is_app_up(atom()) -> boolean(). 30 | is_app_up(App) -> 31 | riak_core_node_watcher:nodes(App) /= []. 32 | 33 | %% @doc Path for the /priv dir of the riak_control app. 34 | -spec priv_dir() -> string(). 35 | priv_dir() -> 36 | code:priv_dir(?MODULE). 37 | -------------------------------------------------------------------------------- /src/riak_control_app.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------ 2 | %% 3 | %% Copyright (c) 2011 Basho Technologies, Inc. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------ 20 | %% 21 | %% @doc Application. 22 | 23 | -module(riak_control_app). 24 | 25 | -behaviour(application). 26 | 27 | %% Application callbacks 28 | -export([start/2, 29 | stop/1]). 30 | 31 | %% ================================================================== 32 | %% Application callbacks 33 | %% ================================================================== 34 | 35 | start(_StartType, _StartArgs) -> 36 | case riak_control_sup:start_link() of 37 | {error, Reason} -> 38 | {error, Reason}; 39 | {ok, Pid} -> 40 | riak_core_capability:register({riak_control, member_info_version}, 41 | [v1, v0], 42 | v0), 43 | {ok, Pid} 44 | end. 45 | 46 | stop(_State) -> 47 | ok. 48 | -------------------------------------------------------------------------------- /src/riak_control_ring.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Basho Technologies, Inc. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | %% 21 | %% @doc Helper utilities for dealing with the ring. 22 | 23 | -module(riak_control_ring). 24 | 25 | -export([ring/0, 26 | n_val/0, 27 | status/2, 28 | status/3]). 29 | 30 | -include("riak_control.hrl"). 31 | 32 | %% @doc Return the ring. 33 | -spec ring() -> riak_core_ring:riak_core_ring(). 34 | ring() -> 35 | {ok, Ring} = riak_core_ring_manager:get_my_ring(), 36 | Ring. 37 | 38 | %% @doc Return the current default n_val. 39 | -spec n_val() -> number(). 40 | n_val() -> 41 | {ok, Props} = application:get_env(riak_core, default_bucket_props), 42 | {n_val, NVal} = lists:keyfind(n_val, 1, Props), 43 | NVal. 44 | 45 | %% @doc Return list of nodes, available partition and quorum. 46 | -spec status(riak_core_ring:riak_core_ring(), list(node())) -> 47 | list({number(), number(), number()}). 48 | status(Ring, Unavailable) -> 49 | NVal = n_val(), 50 | Quorum = (NVal div 2) + 1, 51 | status(Ring, NVal, Quorum, Unavailable). 52 | 53 | %% @doc Return list of nodes, available partition and quorum. 54 | -spec status(riak_core_ring:riak_core_ring(), number(), list(node())) -> 55 | list({number(), number(), number()}). 56 | status(Ring, NVal, Unavailable) -> 57 | Quorum = mochinum:int_ceil(NVal / 2), 58 | status(Ring, NVal, Quorum, Unavailable). 59 | 60 | %% @doc Return list of nodes, available partition and quorum. 61 | -spec status(riak_core_ring:riak_core_ring(), number(), number(), list(node())) -> 62 | list({number(), number(), number()}). 63 | status(Ring, NVal, Quorum, Unavailable) -> 64 | Preflists = riak_core_ring:all_preflists(Ring, NVal), 65 | {Status, _, _, _} = lists:foldr(fun fold_preflist_proplist/2, 66 | {[], NVal, Quorum, Unavailable}, 67 | Preflists), 68 | lists:usort(fun sort_preflist_proplist/2, Status). 69 | 70 | %% @doc Perform a binary conversion of the index to be returned to th 71 | %% user. 72 | -spec pretty_index(number()) -> binary(). 73 | pretty_index(Index) -> 74 | list_to_binary(integer_to_list(Index)). 75 | 76 | %% @doc Sort the preference-as-proplist data structures by index. 77 | -spec sort_preflist_proplist(list(), list()) -> boolean(). 78 | sort_preflist_proplist(A, B) -> 79 | proplists:get_value(index, A) < proplists:get_value(index, B). 80 | 81 | %% @doc Fold over the preference lists and generate a data structure 82 | %% representing partitions in the cluster along with primary 83 | %% replicas. 84 | -spec fold_preflist_proplist(list(), 85 | {list(), integer(), integer(), list(node())}) -> 86 | {list(), integer(), integer(), list(node())}. 87 | fold_preflist_proplist(Preflist, 88 | {Status0, NVal, Quorum, Unavailable}) -> 89 | [{Index, _}|_] = Preflist, 90 | 91 | All = [Node || {_, Node} <- Preflist], 92 | 93 | {Down, Up} = lists:partition(fun(Node) -> 94 | lists:member(Node, Unavailable) end, All), 95 | 96 | UUp = lists:usort(Up), 97 | UAll = lists:usort(All), 98 | UDown = lists:usort(Down), 99 | 100 | Status = [[{quorum, Quorum}, 101 | {distinct, length(UAll) =:= length(All)}, 102 | {index, pretty_index(Index)}, 103 | {available, length(Up)}, 104 | {unavailable_nodes, UDown}, 105 | {available_nodes, UUp}, 106 | {all_nodes, UAll}]|Status0], 107 | 108 | {Status, NVal, Quorum, Unavailable}. 109 | -------------------------------------------------------------------------------- /src/riak_control_routes.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2011 Basho Technologies, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | %% 21 | %% @doc Route helpers. 22 | 23 | -module(riak_control_routes). 24 | 25 | -export([admin_route/1, 26 | nodes_route/0, 27 | cluster_route/0, 28 | handoffs_route/0, 29 | partitions_route/0]). 30 | 31 | %% @doc Provide a resource that all resources sit under. 32 | -spec admin_route(list()) -> list(). 33 | admin_route(Rest) -> 34 | ["admin"|Rest]. 35 | 36 | %% @doc Return route for node resource. 37 | -spec nodes_route() -> list(). 38 | nodes_route() -> 39 | admin_route(["nodes"]). 40 | 41 | %% @doc Return route for node resource. 42 | -spec handoffs_route() -> list(). 43 | handoffs_route() -> 44 | admin_route(["handoffs"]). 45 | 46 | %% @doc Return route for partition resource. 47 | -spec partitions_route() -> list(). 48 | partitions_route() -> 49 | admin_route(["partitions"]). 50 | 51 | %% @doc Return route for cluster resource. 52 | -spec cluster_route() -> list(). 53 | cluster_route() -> 54 | admin_route(["cluster"]). 55 | -------------------------------------------------------------------------------- /src/riak_control_security.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2011 Basho Technologies, Inc. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | %% 21 | %% @doc SSL and Authorization enforcement for administration URLs. 22 | 23 | -module(riak_control_security). 24 | 25 | -export([scheme_is_available/2, 26 | enforce_auth/2, 27 | csrf_token/2, 28 | is_valid_csrf_token/2, 29 | is_null_origin/1, 30 | is_protected/2]). 31 | 32 | -include("riak_control.hrl"). 33 | 34 | -type context() :: term() | undefined. 35 | -type csrf_token() :: list() | undefined. 36 | 37 | %% @doc Enforce use of HTTPS only when a valid auth scheme is enabled. 38 | -spec scheme_is_available(wrq:reqdata(), context()) -> 39 | {boolean(), wrq:reqdata(), context()}. 40 | scheme_is_available(RD, Ctx) -> 41 | case app_helper:get_env(riak_control, force_ssl, undefined) of 42 | undefined -> 43 | %% Handle upgrade, where we want to preserve existing 44 | %% behavior. 45 | case app_helper:get_env(riak_control, auth, none) of 46 | none -> 47 | {true, RD, Ctx}; 48 | _ -> 49 | redirect_if_not_ssl(RD, Ctx) 50 | end; 51 | true -> 52 | redirect_if_not_ssl(RD, Ctx); 53 | _ -> 54 | {true, RD, Ctx} 55 | end. 56 | 57 | %% @doc Redirect if request is not SSL. 58 | -spec redirect_if_not_ssl(wrq:reqdata(), context()) -> 59 | {boolean(), wrq:reqdata(), context()}. 60 | redirect_if_not_ssl(ReqData, Context) -> 61 | case wrq:scheme(ReqData) of 62 | https -> 63 | {true, ReqData, Context}; 64 | _ -> 65 | https_redirect(ReqData, Context) 66 | end. 67 | 68 | %% @doc Perform http redirect to ssl. 69 | -spec https_redirect(wrq:reqdata(), context()) -> 70 | {{halt, 303}, wrq:reqdata(), context()}. 71 | https_redirect(RD,Ctx) -> 72 | Path = wrq:raw_path(RD), 73 | Host = wrq:sock(RD), 74 | Bindings = app_helper:get_env(riak_core, https, []), 75 | Location = case lists:keyfind(Host, 1, Bindings) of 76 | {_, Port} -> 77 | ["https://", Host, ":", integer_to_list(Port), Path]; 78 | _ -> 79 | ["https://", Host, Path] 80 | end, 81 | {{halt,303}, wrq:set_resp_header("Location", Location, RD), Ctx}. 82 | 83 | %% @doc Intended to be called from a webmachine resource's 84 | %% is_authorized function. The return value is a valid resource 85 | %% return value (`{Result, ReqData, Context}'). 86 | %% 87 | %% This function checks for valid authentication in the request. If 88 | %% the authentication is valid, `true' is returned. If it is invalid, 89 | %% the value for the response WWW-Authenticate header is returned. 90 | %% 91 | %% The correct credentials are controled by the appenv 92 | %% `riak_control:auth'. Valid values include: 93 | %% 94 | %% - `userlist' :: `riak_control:userlist' will contain a list of 95 | %% {"user","pass"} pairs that are used. 96 | %% 97 | %% - `none' :: No authentication. 98 | %% 99 | -spec enforce_auth(wrq:reqdata(), context()) -> 100 | {boolean(), wrq:reqdata(), context()}. 101 | enforce_auth(RD, Ctx) -> 102 | case app_helper:get_env(riak_control,auth,none) of 103 | none -> 104 | {true, RD, Ctx}; 105 | Auth -> 106 | case wrq:get_req_header("authorization", RD) of 107 | "Basic "++Base64 -> 108 | enforce_basic_auth(RD, Ctx, Base64, Auth); 109 | _ -> 110 | {?ADMIN_AUTH_HEAD, RD, Ctx} 111 | end 112 | end. 113 | 114 | %% @doc Enforce basic auth. 115 | -spec enforce_basic_auth(wrq:reqdata(), context(), term(), atom()) -> 116 | {boolean(), wrq:reqdata(), context()}. 117 | enforce_basic_auth(RD, Ctx, Base64, Auth) -> 118 | Str = base64:mime_decode_to_string(Base64), 119 | case string:tokens(Str, ":") of 120 | [User, Pass] -> 121 | enforce_user_pass(RD, Ctx, User, Pass, Auth); 122 | _ -> 123 | {?ADMIN_AUTH_HEAD, RD, Ctx} 124 | end. 125 | 126 | %% @doc Enforce user and password match. 127 | -spec enforce_user_pass(wrq:reqdata(), context(), nonempty_string(), 128 | nonempty_string(), atom()) -> 129 | {boolean(), wrq:reqdata(), context()}. 130 | enforce_user_pass(RD, Ctx, User, Pass, Auth) -> 131 | case valid_userpass(User, Pass, Auth) of 132 | true -> 133 | {true, RD, Ctx}; 134 | _ -> 135 | {?ADMIN_AUTH_HEAD, RD, Ctx} 136 | end. 137 | 138 | %% @doc Validate username and password given a particular auth style. 139 | -spec valid_userpass(nonempty_string(), nonempty_string(), 140 | atom()) -> boolean(). 141 | valid_userpass(_User, _Pass, none) -> 142 | true; 143 | valid_userpass(User, Pass, userlist) -> 144 | Users = app_helper:get_env(riak_control, userlist, []), 145 | proplists:get_value(User, Users) == Pass; 146 | valid_userpass(_User, _Pass, _Auth) -> 147 | error_logger:warning_msg("Unknown auth type '~p'", [_Auth]), 148 | false. 149 | 150 | %% @doc Generate a new CSRF token. 151 | -spec csrf_token(wrq:reqdata(), context()) -> csrf_token(). 152 | csrf_token(ReqData, Context) -> 153 | case get_csrf_token(ReqData, Context) of 154 | undefined -> 155 | binary_to_list(riak_core_base64url:encode(crypto:rand_bytes(256))); 156 | Token -> 157 | Token 158 | end. 159 | 160 | %% @doc Get the CSRF token from the cookie. 161 | -spec get_csrf_token(wrq:reqdata(), context()) -> csrf_token(). 162 | get_csrf_token(ReqData, _Context) -> 163 | wrq:get_cookie_value("csrf_token", ReqData). 164 | 165 | %% @doc Ensure this request contains a valid csrf protection token. 166 | -spec is_valid_csrf_token(wrq:reqdata(), context()) -> boolean(). 167 | is_valid_csrf_token(ReqData, Context) -> 168 | HeaderToken = wrq:get_req_header("X-CSRF-Token", ReqData), 169 | CookieToken = get_csrf_token(ReqData, Context), 170 | HeaderToken /= undefined andalso HeaderToken == CookieToken. 171 | 172 | %% @doc Is this a protected method? 173 | -spec is_protected_method(wrq:reqdata()) -> boolean(). 174 | is_protected_method(ReqData) -> 175 | Method = wrq:method(ReqData), 176 | Method == 'POST' orelse Method == 'PUT'. 177 | 178 | %% @doc Is this a protected? 179 | -spec is_protected(wrq:reqdata(), context()) -> boolean(). 180 | is_protected(ReqData, Context) -> 181 | (is_null_origin(ReqData) or 182 | not is_valid_csrf_token(ReqData, Context)) and 183 | is_protected_method(ReqData). 184 | 185 | %% @doc Check if the Origin header is "null". This is useful to look for 186 | %% attempts at CSRF, but is not a complete answer to the problem. 187 | -spec is_null_origin(wrq:reqdata()) -> boolean(). 188 | is_null_origin(ReqData) -> 189 | case wrq:get_req_header("Origin", ReqData) of 190 | "null" -> 191 | true; 192 | _ -> 193 | false 194 | end. 195 | -------------------------------------------------------------------------------- /src/riak_control_sup.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2011 Basho Technologies, Inc. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | %% 21 | %% @doc Application supervisor. 22 | 23 | -module(riak_control_sup). 24 | 25 | -behaviour(supervisor). 26 | 27 | %% API 28 | -export([start_link/0]). 29 | 30 | %% Supervisor callbacks 31 | -export([init/1]). 32 | 33 | %% Helper macro for declaring children of supervisor 34 | -define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). 35 | 36 | %% =================================================================== 37 | %% API functions 38 | %% =================================================================== 39 | 40 | start_link() -> 41 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 42 | 43 | %% =================================================================== 44 | %% Supervisor callbacks 45 | %% =================================================================== 46 | 47 | init([]) -> 48 | RiakControlSession={riak_control_session, 49 | {riak_control_session, start_link, []}, 50 | permanent, 51 | 5000, 52 | worker, 53 | [riak_control_session]}, 54 | 55 | %% determine if riak_control is enabled or not 56 | case app_helper:get_env(riak_control, enabled, false) of 57 | true -> 58 | Resources = [riak_control_wm_gui, 59 | riak_control_wm_cluster, 60 | riak_control_wm_nodes, 61 | riak_control_wm_partitions], 62 | Routes = lists:append([Resource:routes() || Resource <- Resources]), 63 | _ = [webmachine_router:add_route(R) || R <- Routes], 64 | 65 | %% start riak control 66 | {ok, { {one_for_one, 5, 10}, [RiakControlSession] } }; 67 | _ -> 68 | {ok, { {one_for_one, 5, 10}, [] } } 69 | end. 70 | -------------------------------------------------------------------------------- /src/riak_control_wm_gui.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2011 Basho Technologies, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | %% 21 | %% @doc Provides a resource for serving up the GUI skeleton. 22 | 23 | -module(riak_control_wm_gui). 24 | 25 | -export([routes/0, 26 | init/1, 27 | content_types_provided/2, 28 | resource_exists/2, 29 | previously_existed/2, 30 | moved_permanently/2, 31 | to_resource/2, 32 | is_authorized/2, 33 | service_available/2 34 | ]). 35 | 36 | %% riak_control and webmachine dependencies 37 | -include("riak_control.hrl"). 38 | -include_lib("webmachine/include/webmachine.hrl"). 39 | 40 | -record(ctx, { 41 | base_url, 42 | base_path 43 | }). 44 | -type context() :: #ctx{}. 45 | 46 | %% mappings to the various content types supported for this resource 47 | -define(CONTENT_TYPES,[{"text/css", to_resource}, 48 | {"text/html",to_resource}, 49 | {"text/plain",to_resource}, 50 | {"text/javascript",to_resource} 51 | ]). 52 | 53 | %% defines the webmachine routes this module handles 54 | -spec routes() -> [webmachine_dispatcher:matchterm()]. 55 | routes() -> 56 | [{riak_control_routes:admin_route([]),?MODULE,index}, 57 | {riak_control_routes:admin_route(["ui",'*']),?MODULE,undefined}, 58 | {riak_control_routes:admin_route(["ui","index.html"]),?MODULE,oldindex} 59 | ]. 60 | 61 | %% entry-point for the resource from webmachine 62 | -spec init(any()) -> {ok, any()}. 63 | init(Resource) -> {ok,Resource}. 64 | 65 | %% redirect to the index page if no file given 66 | -spec moved_permanently(wrq:reqdata(), oldindex|context()) -> 67 | {false|{true, string()}, wrq:reqdata(), index|context()}. 68 | moved_permanently(Req,oldindex) -> {{true,"/admin"},Req,index}; 69 | moved_permanently(Req,Ctx) -> {false,Req,Ctx}. 70 | 71 | %% the index file isn't here 72 | -spec previously_existed(wrq:reqdata(), context()|oldindex) -> 73 | {boolean(), wrq:reqdata(), context()|oldindex}. 74 | previously_existed(Req,oldindex) -> {true,Req,oldindex}; 75 | previously_existed(Req,Ctx) -> {false,Req,Ctx}. 76 | 77 | %% a resource other than the index is here 78 | -spec resource_exists(wrq:reqdata(), context()|oldindex) -> 79 | {boolean(), wrq:reqdata(), context()|oldindex}. 80 | resource_exists (Req,oldindex) -> {false,Req,oldindex}; 81 | resource_exists (Req,Ctx) -> {true,Req,Ctx}. 82 | 83 | %% redirect to SSL port if using HTTP 84 | -spec service_available(wrq:reqdata(), context()) -> 85 | {boolean() | {halt, non_neg_integer()}, wrq:reqdata(), context()}. 86 | service_available(RD,C) -> 87 | riak_control_security:scheme_is_available(RD,C). 88 | 89 | %% validate username and password 90 | -spec is_authorized(wrq:reqdata(), context()) -> 91 | {true | string(), wrq:reqdata(), context()}. 92 | is_authorized(RD,C) -> 93 | riak_control_security:enforce_auth(RD,C). 94 | 95 | %% return the list of available content types for webmachine 96 | -spec content_types_provided(wrq:reqdata(), context()) -> 97 | {[{ContentType::string(), HandlerFunction::atom()}], 98 | wrq:reqdata(), context()}. 99 | content_types_provided(Req,Ctx=index) -> 100 | {[{"text/html", to_resource}],Req, Ctx}; 101 | content_types_provided(Req,Ctx) -> 102 | Index = file_path(Req), 103 | MimeType = webmachine_util:guess_mime(Index), 104 | {[{MimeType, to_resource}],Req, Ctx}. 105 | 106 | %% return file path 107 | -spec file_path(wrq:reqdata()) -> string(). 108 | file_path(Req) -> 109 | Path=wrq:path_tokens(Req), 110 | filename:join([riak_control:priv_dir(),"admin"] ++ Path). 111 | 112 | %% loads a resource file from disk and returns it 113 | -spec get_file(wrq:reqdata()) -> 114 | binary(). 115 | get_file(Req) -> 116 | Index = file_path(Req), 117 | {ok,Source}=file:read_file(Index), 118 | Source. 119 | 120 | %% respond to an index request 121 | -spec to_resource(wrq:reqdata(), context()) -> 122 | {binary(), wrq:reqdata(), context()}. 123 | to_resource(Req,Ctx=index) -> 124 | Token = riak_control_security:csrf_token(Req, Ctx), 125 | {ok, Content} = index_dtl:render([{csrf_token, Token}]), 126 | {Content, wrq:set_resp_header("Set-Cookie", "csrf_token="++Token++"; httponly", Req), Ctx}; 127 | 128 | %% respond to a file request 129 | to_resource(Req,Ctx) -> 130 | {get_file(Req),Req,Ctx}. 131 | -------------------------------------------------------------------------------- /src/riak_control_wm_nodes.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2011 Basho Technologies, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | %% 21 | %% @doc Returns a list of all nodes, and information about their 22 | %% membership in the cluster. 23 | 24 | -module(riak_control_wm_nodes). 25 | 26 | -export([routes/0, 27 | init/1, 28 | to_json/2, 29 | forbidden/2, 30 | is_authorized/2, 31 | service_available/2, 32 | content_types_provided/2]). 33 | 34 | -include("riak_control.hrl"). 35 | -include_lib("webmachine/include/webmachine.hrl"). 36 | 37 | %% @doc Return routes this resource should respond to. 38 | -spec routes() -> [webmachine_dispatcher:matchterm()]. 39 | routes() -> 40 | [{riak_control_routes:nodes_route(), ?MODULE, []}]. 41 | 42 | %% @doc Initialize resource. 43 | -spec init([]) -> {ok, undefined}. 44 | init([]) -> 45 | {ok, undefined}. 46 | 47 | %% @doc Prevent requests coming from an invalid origin. 48 | -spec forbidden(wrq:reqdata(), undefined) -> 49 | {boolean(), wrq:reqdata(), undefined}. 50 | forbidden(ReqData, Context) -> 51 | {riak_control_security:is_protected(ReqData, Context), ReqData, Context}. 52 | 53 | %% @doc Handle SSL requests. 54 | -spec service_available(wrq:reqdata(), undefined) -> 55 | {boolean(), wrq:reqdata(), undefined}. 56 | service_available(ReqData, Context) -> 57 | riak_control_security:scheme_is_available(ReqData, Context). 58 | 59 | %% @doc Ensure user has access. 60 | -spec is_authorized(wrq:reqdata(), undefined) -> 61 | {boolean(), wrq:reqdata(), undefined}. 62 | is_authorized(ReqData, Context) -> 63 | riak_control_security:enforce_auth(ReqData, Context). 64 | 65 | %% @doc Return content-types which are provided. 66 | -spec content_types_provided(wrq:reqdata(), undefined) -> 67 | {list(), wrq:reqdata(), undefined}. 68 | content_types_provided(ReqData, Context) -> 69 | {[{"application/json", to_json}], ReqData, Context}. 70 | 71 | %% @doc Return the current cluster, along with a plan if it's available. 72 | -spec to_json(wrq:reqdata(), undefined) -> {binary(), wrq:reqdata(), undefined}. 73 | to_json(ReqData, Context) -> 74 | %% Get the current node list. 75 | {ok, _V, RawNodes} = riak_control_session:get_nodes(), 76 | 77 | Nodes = [jsonify_node(Node) || Node=?MEMBER_INFO{} <- RawNodes], 78 | Encoded = mochijson2:encode({struct, [{nodes, Nodes}]}), 79 | 80 | {Encoded, ReqData, Context}. 81 | 82 | %% @doc Turn a node into a proper struct for serialization. 83 | -spec jsonify_node(member()) -> {struct, list()}. 84 | jsonify_node(Node) -> 85 | LWM=app_helper:get_env(riak_control,low_mem_watermark,0.1), 86 | MemUsed = Node?MEMBER_INFO.mem_used, 87 | MemTotal = Node?MEMBER_INFO.mem_total, 88 | Reachable = Node?MEMBER_INFO.reachable, 89 | LowMem = low_mem(Reachable, MemUsed, MemTotal, LWM), 90 | {struct,[{"name",Node?MEMBER_INFO.node}, 91 | {"status",Node?MEMBER_INFO.status}, 92 | {"reachable",Reachable}, 93 | {"ring_pct",Node?MEMBER_INFO.ring_pct}, 94 | {"pending_pct",Node?MEMBER_INFO.pending_pct}, 95 | {"mem_total",MemTotal}, 96 | {"mem_used",MemUsed}, 97 | {"mem_erlang",Node?MEMBER_INFO.mem_erlang}, 98 | {"low_mem",LowMem}, 99 | {"me",Node?MEMBER_INFO.node == node()}, 100 | {"action",Node?MEMBER_INFO.action}, 101 | {"replacement",Node?MEMBER_INFO.replacement}]}. 102 | 103 | %% @doc Determine if a node has low memory. 104 | -spec low_mem(boolean(), number() | atom(), number() | atom(), number()) 105 | -> boolean(). 106 | low_mem(Reachable, MemUsed, MemTotal, LWM) -> 107 | case Reachable of 108 | false -> 109 | false; 110 | true -> 111 | %% There is a race where the node is online, but memsup is 112 | %% still starting so memory is unavailable. 113 | case MemTotal of 114 | undefined -> 115 | false; 116 | _ -> 117 | 1.0 - (MemUsed/MemTotal) < LWM 118 | end 119 | end. 120 | -------------------------------------------------------------------------------- /src/riak_control_wm_partitions.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2011 Basho Technologies, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | %% 21 | %% @doc Returns a list of all partitions, how many primary replicas 22 | %% are available, what the current n_val and quorum 23 | %% configuration is, as well as the unavailable nodes for 24 | %% each partition. 25 | 26 | -module(riak_control_wm_partitions). 27 | 28 | -export([routes/0, 29 | init/1, 30 | content_types_provided/2, 31 | to_json/2, 32 | is_authorized/2, 33 | service_available/2, 34 | forbidden/2]). 35 | 36 | -include("riak_control.hrl"). 37 | -include_lib("webmachine/include/webmachine.hrl"). 38 | 39 | -define(CONTENT_TYPES, [{"application/json",to_json}]). 40 | 41 | -define(VNODE_TYPES, [riak_kv,riak_pipe,riak_search]). 42 | 43 | -record(context, {partitions, default_n_val}). 44 | -type context() :: #context{}. 45 | 46 | %% @doc Route handling. 47 | -spec routes() -> [webmachine_dispatcher:matchterm()]. 48 | routes() -> 49 | [{riak_control_routes:partitions_route(), ?MODULE, []}]. 50 | 51 | %% @doc Get partition list at the start of the request. 52 | -spec init(list()) -> {ok, context()}. 53 | init([]) -> 54 | {ok, _, Partitions} = riak_control_session:get_partitions(), 55 | {ok, _, DefNVal} = riak_control_session:get_default_n_val(), 56 | {ok, #context{partitions=Partitions, default_n_val=DefNVal}}. 57 | 58 | %% @doc Validate origin. 59 | -spec forbidden(wrq:reqdata(), context()) -> 60 | {boolean(), wrq:reqdata(), context()}. 61 | forbidden(ReqData, Context) -> 62 | {riak_control_security:is_null_origin(ReqData), ReqData, Context}. 63 | 64 | %% @doc Determine if it's available. 65 | -spec service_available(wrq:reqdata(), context()) -> 66 | {boolean() | {halt, non_neg_integer()}, wrq:reqdata(), context()}. 67 | service_available(ReqData, Context) -> 68 | riak_control_security:scheme_is_available(ReqData, Context). 69 | 70 | %% @doc Handle authorization. 71 | -spec is_authorized(wrq:reqdata(), context()) -> 72 | {true | string(), wrq:reqdata(), context()}. 73 | is_authorized(ReqData, Context) -> 74 | riak_control_security:enforce_auth(ReqData, Context). 75 | 76 | %% @doc Return available content types. 77 | -spec content_types_provided(wrq:reqdata(), context()) -> 78 | {[{ContentType::string(), HandlerFunction::atom()}], wrq:reqdata(), context()}. 79 | content_types_provided(ReqData, Context) -> 80 | {?CONTENT_TYPES, ReqData, Context}. 81 | 82 | %% @doc Return a list of partitions. 83 | -spec to_json(wrq:reqdata(),context()) -> 84 | {iolist(), wrq:reqdata(), context()}. 85 | to_json(ReqData, Context=#context{partitions=Partitions, default_n_val=DefNVal}) -> 86 | Encoded = mochijson2:encode({struct,[{partitions, Partitions}, 87 | {default_n_val, DefNVal}]}), 88 | {Encoded, ReqData, Context}. 89 | -------------------------------------------------------------------------------- /templates/index.dtl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Riak Control 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICvTCCAiYCCQDgxT3HogRJ/TANBgkqhkiG9w0BAQUFADCBojELMAkGA1UEBhMC 3 | VVMxFjAUBgNVBAgTDU1hc3NhY2h1c2V0dHMxEjAQBgNVBAcTCUNhbWJyaWRnZTEf 4 | MB0GA1UEChMWQmFzaG8gVGVjaG5vbG9naWVzIEluYzENMAsGA1UECxMEUmlhazEY 5 | MBYGA1UEAxMPSmVmZnJleSBNYXNzdW5nMR0wGwYJKoZIhvcNAQkBFg5qZWZmQGJh 6 | c2hvLmNvbTAeFw0xMTEwMzExNjQ3NTNaFw0yMTEwMjgxNjQ3NTNaMIGiMQswCQYD 7 | VQQGEwJVUzEWMBQGA1UECBMNTWFzc2FjaHVzZXR0czESMBAGA1UEBxMJQ2FtYnJp 8 | ZGdlMR8wHQYDVQQKExZCYXNobyBUZWNobm9sb2dpZXMgSW5jMQ0wCwYDVQQLEwRS 9 | aWFrMRgwFgYDVQQDEw9KZWZmcmV5IE1hc3N1bmcxHTAbBgkqhkiG9w0BCQEWDmpl 10 | ZmZAYmFzaG8uY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCkys/CB8Ce 11 | fO19JsFiL2K5pODMWFmXxfQgvARB2rIvoJ4R4mNKI639xRbR+gCPreJvZRw8trgD 12 | 8sARFv8J1SlqqYRDN8zfMlvolXh6Atujou/LwjUpTA3pMe9lZrWU1+JZOMAk79lz 13 | O/1etfR12By0SqNfDgUMIIZST7i3Fw2IkwIDAQABMA0GCSqGSIb3DQEBBQUAA4GB 14 | ABTOXMBYoEn0biwqaDcZzInZvZHupFkmkOABumgJ5xVDQ/9LIzy9mfg0Ko+JERlM 15 | 0w0dliu2sfFoXLps9EohjIzrU1B3CwSNvNqPmcj9V4k0iXrsvDfbG9eJ9nYaUY0Y 16 | L5I9/KAOIf3fEmnFbjtmyiLVhrM6kBB3fvoAVQfwL6cZ 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /test/javascripts/admin: -------------------------------------------------------------------------------- 1 | ../../priv/admin/ -------------------------------------------------------------------------------- /test/javascripts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Riak Control Test Suite 6 | 7 | 20 | 21 | 22 |
23 |
24 |
25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /test/javascripts/pages_test.js: -------------------------------------------------------------------------------- 1 | module("Pages", { 2 | setup: function() { 3 | RiakControl.reset(); 4 | RiakControl.Node.FIXTURES = [{ 5 | id: "low_mem_node", 6 | status: "valid", 7 | reachable: true, 8 | ring_pct: 0.1875, 9 | pending_pct: 0.1875, 10 | mem_total: 100, 11 | mem_used: 100, 12 | mem_erlang: 100, 13 | low_mem: true, 14 | me: true, 15 | claimant: false 16 | }]; 17 | 18 | $.mockjax({ 19 | url: "/admin/cluster", 20 | responseText: MockResponses.ClusterResponse 21 | }); 22 | 23 | $.mockjax({ 24 | url: "/admin/partitions", 25 | responseText: MockResponses.PartitionsResponse 26 | }); 27 | 28 | $.mockjaxSettings.logging = false; 29 | }, 30 | 31 | teardown: function() { 32 | $.mockjaxClear(); 33 | } 34 | }); 35 | 36 | test("snapshot page shows problematic nodes", function() { 37 | visit("/").then(function() { 38 | equal(find("#snapshot-headline").text(), "Current Snapshot"); 39 | equal(find("#low_mem-nodes-list li").length, 1, "Detects 1 low memory node"); 40 | }); 41 | }); 42 | 43 | test("cluster page lists changes", function() { 44 | visit("/cluster").then(function() { 45 | equal(find("#cluster-headline").text(), "Cluster Management"); 46 | ok(find("#planned-area").text().indexOf("leave") !== -1, "Displays the leaving node"); 47 | }); 48 | }); 49 | 50 | test("nodes page lists nodes", function() { 51 | visit("/nodes").then(function() { 52 | equal(find("#node-headline").text(), "Node Management"); 53 | ok(find("#node-list").text().indexOf("low_mem_node") !== -1, "Displays the low mem node") 54 | }); 55 | }) 56 | 57 | test("ring page shows partitions", function() { 58 | visit("/ring").then(function() { 59 | equal(find("#ring-headline").text(), "Ring Status"); 60 | equal(find("#partition-container div div.ember-view").length, 64); 61 | }); 62 | }) 63 | -------------------------------------------------------------------------------- /test/javascripts/runner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * QtWebKit-powered headless test runner using PhantomJS 3 | * 4 | * PhantomJS binaries: http://phantomjs.org/download.html 5 | * Requires PhantomJS 1.6+ (1.7+ recommended) 6 | * 7 | * Run with: 8 | * phantomjs runner.js [url-of-your-qunit-testsuite] 9 | * 10 | * e.g. 11 | * phantomjs runner.js http://localhost/qunit/test/index.html 12 | */ 13 | 14 | /*global phantom:false, require:false, console:false, window:false, QUnit:false */ 15 | 16 | (function() { 17 | 'use strict'; 18 | 19 | var url, page, timeout, 20 | args = require('system').args; 21 | 22 | // arg[0]: scriptName, args[1...]: arguments 23 | if (args.length < 2 || args.length > 3) { 24 | console.error('Usage:\n phantomjs runner.js [url-of-your-qunit-testsuite] [timeout-in-seconds]'); 25 | phantom.exit(1); 26 | } 27 | 28 | url = args[1]; 29 | page = require('webpage').create(); 30 | if (args[2] !== undefined) { 31 | timeout = parseInt(args[2], 10); 32 | } 33 | 34 | // Route `console.log()` calls from within the Page context to the main Phantom context (i.e. current `this`) 35 | page.onConsoleMessage = function(msg) { 36 | console.log(msg); 37 | }; 38 | 39 | page.onInitialized = function() { 40 | page.evaluate(addLogging); 41 | }; 42 | 43 | page.onCallback = function(message) { 44 | var result, 45 | failed; 46 | 47 | if (message) { 48 | if (message.name === 'QUnit.done') { 49 | result = message.data; 50 | failed = !result || result.failed; 51 | 52 | phantom.exit(failed ? 1 : 0); 53 | } 54 | } 55 | }; 56 | 57 | page.open(url, function(status) { 58 | if (status !== 'success') { 59 | console.error('Unable to access network: ' + status); 60 | phantom.exit(1); 61 | } else { 62 | // Cannot do this verification with the 'DOMContentLoaded' handler because it 63 | // will be too late to attach it if a page does not have any script tags. 64 | var qunitMissing = page.evaluate(function() { return (typeof QUnit === 'undefined' || !QUnit); }); 65 | if (qunitMissing) { 66 | console.error('The `QUnit` object is not present on this page.'); 67 | phantom.exit(1); 68 | } 69 | 70 | // Set a timeout on the test running, otherwise tests with async problems will hang forever 71 | if (typeof timeout === 'number') { 72 | setTimeout(function() { 73 | console.error('The specified timeout of ' + timeout + ' seconds has expired. Aborting...'); 74 | phantom.exit(1); 75 | }, timeout * 1000); 76 | } 77 | 78 | // Do nothing... the callback mechanism will handle everything! 79 | } 80 | }); 81 | 82 | function addLogging() { 83 | window.document.addEventListener('DOMContentLoaded', function() { 84 | var currentTestAssertions = []; 85 | 86 | QUnit.log(function(details) { 87 | var response; 88 | 89 | // Ignore passing assertions 90 | if (details.result) { 91 | return; 92 | } 93 | 94 | response = details.message || ''; 95 | 96 | if (typeof details.expected !== 'undefined') { 97 | if (response) { 98 | response += ', '; 99 | } 100 | 101 | response += 'expected: ' + details.expected + ', but was: ' + details.actual; 102 | } 103 | 104 | if (details.source) { 105 | response += "\n" + details.source; 106 | } 107 | 108 | currentTestAssertions.push('Failed assertion: ' + response); 109 | }); 110 | 111 | QUnit.testDone(function(result) { 112 | var i, 113 | len, 114 | name = result.module + ': ' + result.name; 115 | 116 | if (result.failed) { 117 | console.log('Test failed: ' + name); 118 | 119 | for (i = 0, len = currentTestAssertions.length; i < len; i++) { 120 | console.log(' ' + currentTestAssertions[i]); 121 | } 122 | } 123 | 124 | currentTestAssertions.length = 0; 125 | }); 126 | 127 | QUnit.done(function(result) { 128 | console.log('Took ' + result.runtime + 'ms to run ' + result.total + ' tests. ' + result.passed + ' passed, ' + result.failed + ' failed.'); 129 | 130 | if (typeof window.callPhantom === 'function') { 131 | window.callPhantom({ 132 | 'name': 'QUnit.done', 133 | 'data': result 134 | }); 135 | } 136 | }); 137 | }, false); 138 | } 139 | })(); 140 | -------------------------------------------------------------------------------- /test/javascripts/support/qunit-1.12.0.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.12.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | overflow: hidden; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | #qunit-modulefilter-container { 81 | float: right; 82 | } 83 | 84 | /** Tests: Pass/Fail */ 85 | 86 | #qunit-tests { 87 | list-style-position: inside; 88 | } 89 | 90 | #qunit-tests li { 91 | padding: 0.4em 0.5em 0.4em 2.5em; 92 | border-bottom: 1px solid #fff; 93 | list-style-position: inside; 94 | } 95 | 96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 97 | display: none; 98 | } 99 | 100 | #qunit-tests li strong { 101 | cursor: pointer; 102 | } 103 | 104 | #qunit-tests li a { 105 | padding: 0.5em; 106 | color: #c2ccd1; 107 | text-decoration: none; 108 | } 109 | #qunit-tests li a:hover, 110 | #qunit-tests li a:focus { 111 | color: #000; 112 | } 113 | 114 | #qunit-tests li .runtime { 115 | float: right; 116 | font-size: smaller; 117 | } 118 | 119 | .qunit-assert-list { 120 | margin-top: 0.5em; 121 | padding: 0.5em; 122 | 123 | background-color: #fff; 124 | 125 | border-radius: 5px; 126 | -moz-border-radius: 5px; 127 | -webkit-border-radius: 5px; 128 | } 129 | 130 | .qunit-collapsed { 131 | display: none; 132 | } 133 | 134 | #qunit-tests table { 135 | border-collapse: collapse; 136 | margin-top: .2em; 137 | } 138 | 139 | #qunit-tests th { 140 | text-align: right; 141 | vertical-align: top; 142 | padding: 0 .5em 0 0; 143 | } 144 | 145 | #qunit-tests td { 146 | vertical-align: top; 147 | } 148 | 149 | #qunit-tests pre { 150 | margin: 0; 151 | white-space: pre-wrap; 152 | word-wrap: break-word; 153 | } 154 | 155 | #qunit-tests del { 156 | background-color: #e0f2be; 157 | color: #374e0c; 158 | text-decoration: none; 159 | } 160 | 161 | #qunit-tests ins { 162 | background-color: #ffcaca; 163 | color: #500; 164 | text-decoration: none; 165 | } 166 | 167 | /*** Test Counts */ 168 | 169 | #qunit-tests b.counts { color: black; } 170 | #qunit-tests b.passed { color: #5E740B; } 171 | #qunit-tests b.failed { color: #710909; } 172 | 173 | #qunit-tests li li { 174 | padding: 5px; 175 | background-color: #fff; 176 | border-bottom: none; 177 | list-style-position: inside; 178 | } 179 | 180 | /*** Passing Styles */ 181 | 182 | #qunit-tests li li.pass { 183 | color: #3c510c; 184 | background-color: #fff; 185 | border-left: 10px solid #C6E746; 186 | } 187 | 188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 189 | #qunit-tests .pass .test-name { color: #366097; } 190 | 191 | #qunit-tests .pass .test-actual, 192 | #qunit-tests .pass .test-expected { color: #999999; } 193 | 194 | #qunit-banner.qunit-pass { background-color: #C6E746; } 195 | 196 | /*** Failing Styles */ 197 | 198 | #qunit-tests li li.fail { 199 | color: #710909; 200 | background-color: #fff; 201 | border-left: 10px solid #EE5757; 202 | white-space: pre; 203 | } 204 | 205 | #qunit-tests > li:last-child { 206 | border-radius: 0 0 5px 5px; 207 | -moz-border-radius: 0 0 5px 5px; 208 | -webkit-border-bottom-right-radius: 5px; 209 | -webkit-border-bottom-left-radius: 5px; 210 | } 211 | 212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 213 | #qunit-tests .fail .test-name, 214 | #qunit-tests .fail .module-name { color: #000000; } 215 | 216 | #qunit-tests .fail .test-actual { color: #EE5757; } 217 | #qunit-tests .fail .test-expected { color: green; } 218 | 219 | #qunit-banner.qunit-fail { background-color: #EE5757; } 220 | 221 | 222 | /** Result */ 223 | 224 | #qunit-testresult { 225 | padding: 0.5em 0.5em 0.5em 2.5em; 226 | 227 | color: #2b81af; 228 | background-color: #D2E0E6; 229 | 230 | border-bottom: 1px solid white; 231 | } 232 | #qunit-testresult .module-name { 233 | font-weight: bold; 234 | } 235 | 236 | /** Fixture */ 237 | 238 | #qunit-fixture { 239 | position: absolute; 240 | top: -10000px; 241 | left: -10000px; 242 | width: 1000px; 243 | height: 1000px; 244 | } 245 | -------------------------------------------------------------------------------- /test/javascripts/test_helper.js: -------------------------------------------------------------------------------- 1 | RiakControl.Store.reopen({ 2 | adapter: DS.FixtureAdapter, 3 | simulateRemoteResponse: false 4 | }); 5 | 6 | RiakControl.store = RiakControl.Store.create(); 7 | // fixtures are assigned in the tests where they are needed 8 | RiakControl.Node.FIXTURES = []; 9 | 10 | RiakControl.rootElement = "#ember-testing"; 11 | RiakControl.setupForTesting(); 12 | RiakControl.injectTestHelpers(); 13 | -------------------------------------------------------------------------------- /test/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQCkys/CB8CefO19JsFiL2K5pODMWFmXxfQgvARB2rIvoJ4R4mNK 3 | I639xRbR+gCPreJvZRw8trgD8sARFv8J1SlqqYRDN8zfMlvolXh6Atujou/LwjUp 4 | TA3pMe9lZrWU1+JZOMAk79lzO/1etfR12By0SqNfDgUMIIZST7i3Fw2IkwIDAQAB 5 | AoGANAW2eolZ/G5xxo2ChQ1yfCqZsMi/V9NtExxnt6Zjk/d/jyPJtnD3D2K1ponm 6 | vXTmQ8ZGmMAR7WUnzv1Ue/UoAntcyXwKAm+T+2IUJiir/qzYKLSn8FJ3wA+OWYKs 7 | 1nSryi54IuNenKUslxMPDPk/0bM6nZS2AvbNPYhX7a8evXkCQQDNdsvDO3Ofn0pJ 8 | +3bMLH5Ch/adrJ0TfF1H8n9pxiuq813ppffXsyzv3haTbGHOtEM5tILYbHmO16h1 9 | vY+hhoHHAkEAzVMVRaDefjp1qKfoyRm9ySa3GgH71t1dvm1jGTRxNk9M8pekLDz+ 10 | GWyTzffM2/+8Xz4RFzLjqAoAjzBGMeAC1QJAANiycjV2fnvbhH6CuMieJIwG2hNx 11 | +jiS8c7v83GbkHK8OlAyuzLDxqE1mpnhtUZM2JoDx/x6a7o7uXB0fQfe1QJBAKxi 12 | d/aYhJS4IjaymqfUm9m5TntgdP9FpcIOdugfdnmhhLochLK7lp7j4QhJZ07B3Hae 13 | Vp0Clc5sb2HIpvaS2+0CQEV5NxPjavmlCQksQvU2OAQvTW3Sm9lahTl4XvdVxfj0 14 | G9sZ2erg7MIo2LF4V6FM6Hbfoj/FhAMOXlXUoUGs1uI= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /test/riak_control_schema_test.erl: -------------------------------------------------------------------------------- 1 | -module(riak_control_schema_test). 2 | 3 | -include_lib("eunit/include/eunit.hrl"). 4 | -compile(export_all). 5 | 6 | basic_schema_test() -> 7 | lager:start(), 8 | Config = cuttlefish_unit:generate_config("../priv/riak_control.schema", []), 9 | 10 | cuttlefish_unit:assert_config(Config, "riak_control.enabled", false), 11 | cuttlefish_unit:assert_config(Config, "riak_control.auth", none), 12 | cuttlefish_unit:assert_not_configured(Config, "riak_control.userlist"), 13 | ok. 14 | 15 | override_test() -> 16 | Conf = [ 17 | {["riak_control"], on}, 18 | {["riak_control", "auth", "mode"], userlist} 19 | ], 20 | Config = cuttlefish_unit:generate_config("../priv/riak_control.schema", Conf), 21 | cuttlefish_unit:assert_config(Config, "riak_control.enabled", true), 22 | cuttlefish_unit:assert_config(Config, "riak_control.auth", userlist), 23 | cuttlefish_unit:assert_not_configured(Config, "riak_control.userlist"), 24 | ok. 25 | 26 | userlist_schema_test() -> 27 | CuttlefishConf = [ 28 | {["riak_control", "auth", "mode"], userlist}, 29 | {["riak_control", "auth", "user", "dev", "password"], "1234"}, 30 | {["riak_control", "auth", "user", "admin", "password"], "5678"} 31 | ], 32 | 33 | Config = cuttlefish_unit:generate_config("../priv/riak_control.schema", CuttlefishConf), 34 | 35 | cuttlefish_unit:assert_config(Config, "riak_control.userlist.dev", "1234"), 36 | cuttlefish_unit:assert_config(Config, "riak_control.userlist.admin", "5678"), 37 | 38 | %% Other than userlist, which we overrode in CuttlefishConf, the others should still be set to defaults: 39 | cuttlefish_unit:assert_config(Config, "riak_control.enabled", false), 40 | cuttlefish_unit:assert_config(Config, "riak_control.auth", userlist), 41 | ok. 42 | -------------------------------------------------------------------------------- /tools.mk: -------------------------------------------------------------------------------- 1 | REBAR ?= ./rebar 2 | 3 | test: compile 4 | ${REBAR} eunit skip_deps=true 5 | 6 | docs: 7 | ${REBAR} doc skip_deps=true 8 | 9 | xref: compile 10 | ${REBAR} xref skip_deps=true 11 | 12 | PLT ?= $(HOME)/.combo_dialyzer_plt 13 | LOCAL_PLT = .local_dialyzer_plt 14 | DIALYZER_FLAGS ?= -Wunmatched_returns 15 | 16 | ${PLT}: compile 17 | @if [ -f $(PLT) ]; then \ 18 | dialyzer --check_plt --plt $(PLT) --apps $(DIALYZER_APPS) && \ 19 | dialyzer --add_to_plt --plt $(PLT) --output_plt $(PLT) --apps $(DIALYZER_APPS) ; test $$? -ne 1; \ 20 | else \ 21 | dialyzer --build_plt --output_plt $(PLT) --apps $(DIALYZER_APPS); test $$? -ne 1; \ 22 | fi 23 | 24 | ${LOCAL_PLT}: compile 25 | @if [ -d deps ]; then \ 26 | if [ -f $(LOCAL_PLT) ]; then \ 27 | dialyzer --check_plt --plt $(LOCAL_PLT) deps/*/ebin && \ 28 | dialyzer --add_to_plt --plt $(LOCAL_PLT) --output_plt $(LOCAL_PLT) deps/*/ebin ; test $$? -ne 1; \ 29 | else \ 30 | dialyzer --build_plt --output_plt $(LOCAL_PLT) deps/*/ebin ; test $$? -ne 1; \ 31 | fi \ 32 | fi 33 | 34 | dialyzer: ${PLT} ${LOCAL_PLT} 35 | @echo "==> $(shell basename $(shell pwd)) (dialyzer)" 36 | @if [ -f $(LOCAL_PLT) ]; then \ 37 | PLTS="$(PLT) $(LOCAL_PLT)"; \ 38 | else \ 39 | PLTS=$(PLT); \ 40 | fi; \ 41 | if [ -f dialyzer.ignore-warnings ]; then \ 42 | if [ $$(grep -cvE '[^[:space:]]' dialyzer.ignore-warnings) -ne 0 ]; then \ 43 | echo "ERROR: dialyzer.ignore-warnings contains a blank/empty line, this will match all messages!"; \ 44 | exit 1; \ 45 | fi; \ 46 | dialyzer $(DIALYZER_FLAGS) --plts $${PLTS} -c ebin > dialyzer_warnings ; \ 47 | egrep -v "^[[:space:]]*(done|Checking|Proceeding|Compiling)" dialyzer_warnings | grep -F -f dialyzer.ignore-warnings -v > dialyzer_unhandled_warnings ; \ 48 | cat dialyzer_unhandled_warnings ; \ 49 | [ $$(cat dialyzer_unhandled_warnings | wc -l) -eq 0 ] ; \ 50 | else \ 51 | dialyzer $(DIALYZER_FLAGS) --plts $${PLTS} -c ebin; \ 52 | fi 53 | 54 | cleanplt: 55 | @echo 56 | @echo "Are you sure? It takes several minutes to re-build." 57 | @echo Deleting $(PLT) and $(LOCAL_PLT) in 5 seconds. 58 | @echo 59 | sleep 5 60 | rm $(PLT) 61 | rm $(LOCAL_PLT) 62 | 63 | --------------------------------------------------------------------------------