http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/public/stylesheets/style.scss ---------------------------------------------------------------------- diff --git a/modules/web-console/src/main/js/public/stylesheets/style.scss b/modules/web-console/src/main/js/public/stylesheets/style.scss new file mode 100644 index 0000000..589028c --- /dev/null +++ b/modules/web-console/src/main/js/public/stylesheets/style.scss @@ -0,0 +1,2128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "font-awesome-custom"; +@import "bootstrap-custom"; + +@import "./variables"; + +@import "./../../app/directives/information/information.scss"; + +@font-face { + font-family: 'Roboto Slab'; + font-style: normal; + font-weight: 400; + src: local('Roboto Slab Regular'), local('RobotoSlab-Regular'), url(//fonts.gstatic.com/s/robotoslab/v6/y7lebkjgREBJK96VQi37ZiwlidHJgAgmTjOEEzwu1L8.ttf) format('truetype'); +} + +@font-face { + font-family: 'Roboto Slab'; + font-style: normal; + font-weight: 700; + src: local('Roboto Slab Bold'), local('RobotoSlab-Bold'), url(//fonts.gstatic.com/s/robotoslab/v6/dazS1PrQQuCxC3iOAJFEJTdGNerWpg2Hn6A-BxWgZ_I.ttf) format('truetype'); +} + +hr { + margin: 20px 0; +} + +.theme-line a.active { + font-weight: bold; + font-size: 1.1em; +} + +.theme-line a:focus { + text-decoration: underline; + outline: none; +} + +.navbar-default .navbar-brand, .navbar-default .navbar-brand:hover { + position: absolute; + left: 0; + text-align: center; +} + +.navbar-brand { + padding: 5px 0; + margin: 10px 0; +} + +.modal.center .modal-dialog { + position: fixed; + top: 50%; + left: 50%; + -webkit-transform: translateX(-50%) translateY(-50%); + transform: translateX(-50%) translateY(-50%); +} + +.border-left { + box-shadow: 1px 0 0 0 $gray-lighter inset; +} + +.border-right { + box-shadow: 1px 0 0 0 $gray-lighter; +} + +.theme-line header { + background-color: $ignite-background-color; +} + +.theme-line .docs-header h1 { + color: $ignite-header-color; + margin-top: 0; + font-size: 22px; +} + +.theme-line .footer { + text-align: center; +} + +.table.table-vertical-middle tbody > tr > td { + vertical-align: middle; +} + +ul.navbar-nav, .sidebar-nav { + li.active > a { + color: $link-color; + } + + li.active > a:not(.dropdown-toggle) { + cursor: default; + pointer-events: none; + } +} + +.theme-line .sidebar-nav { + padding-bottom: 30px; + + ul { + padding: 0; + list-style: none; + margin: 3px 0 0; + + li { + line-height: $input-height; + + a { + font-size: 18px; + color: $ignite-header-color; + position: relative; + white-space: nowrap; + overflow: hidden; + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + + span.fa-stack { + margin-right: 5px; + font-size: 12px; + height: 26px; + } + } + + a:hover { color: $link-hover-color; } + + a.active { + color: $link-color; + } + } + } +} + +.theme-line .sidebar-nav ul li a:hover { + text-decoration: none; +} + +.theme-line .select { + li a.active { + color: $dropdown-link-active-color; + } + + li a:hover { + color: $dropdown-link-hover-color; + } +} + +.theme-line .select, +.theme-line .typeahead { + .active { + font-size: 1em; + background-color: $gray-lighter; + } +} + +.theme-line button.form-control.placeholder { + color: $input-color-placeholder; +} + +.theme-line .summary-pojo-list > ul.dropdown-menu { + width: 100%; + max-width: none; +} + +.tooltip { + word-wrap: break-word; +} + +.theme-line ul.dropdown-menu { + min-width: 120px; + max-width: 280px; + max-height: 20em; + overflow: auto; + overflow-x: hidden; + outline-style: none; + + li > a { + display: block; + + //cursor: default; + padding: 3px 10px; + + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + i { + float: right; + color: $brand-primary; + background-color: transparent; + line-height: $line-height-base; + margin-left: 5px; + margin-right: 0; + } + } + + li.divider { + margin: 3px 0; + } +} + +.theme-line .border-left .sidebar-nav { + padding-left: 15px; +} + +.theme-line .suggest { + padding: 5px; + display: inline-block; + font-size: 12px; +} + +.theme-line header { + border-bottom: 8px solid $ignite-border-bottom-color; + + p { + color: $ignite-header-color; + } +} + +.header .nav.navbar-nav.pull-right > li > a { + padding-right: 0; +} + +.header .title { + margin: 20px 0 5px 0; + padding: 0 15px; + + font-size: 1.4em; +} + +.header .nav.navbar-nav .not-link { + padding: 15px; + display: inline-block; +} + +.nav > li { + > a { + color: $navbar-default-link-color + } + > a:hover { + color: $link-hover-color + } + > a.active { + color: $link-color + } +} + +.theme-line header .navbar-nav a { + line-height: 25px; + font-size: 18px; +} + +.theme-line .section-right { + padding-left: 30px; +} + +.body-overlap .main-content { + margin-top: 30px; +} + +.body-box .main-content, +.body-overlap .main-content { + padding: 30px; + box-shadow: 0 0 0 1px $ignite-border-color; + background-color: $ignite-background-color; +} + +body { + font-weight: 400; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + margin-bottom: 10px; +} + +.container-footer { + margin-top: 20px; + margin-bottom: 20px; + + p { + font-size: 12px; + margin-bottom: 0; + } +} + +/* Modal */ +.modal { + display: block; + overflow: hidden; +} + +.modal .close { + position: absolute; + top: 10px; + right: 10px; + float: none; +} + +.modal-header { + border-top-left-radius: 6px; + border-top-right-radius: 6px; +} + +// Close icon +.modal-header .close { + margin-right: -2px; +} + +.modal .modal-dialog { + width: 650px; +} + +.modal .modal-content { + background-color: $gray-lighter; + + .input-tip { + padding-top: 1px; + } +} + +.modal .modal-content .modal-header { + background-color: $ignite-background-color; + text-align: center; + color: $ignite-header-color; + padding: 15px 25px 15px 15px; +} + +.modal .modal-content .modal-header h4 { + font-size: 22px; +} + +.modal .modal-content .modal-footer { + margin-top: 0; +} + +.modal-footer { + label { + float: left; + margin: 0; + } + + .btn:last-child { + margin-right: 0; + } + + .checkbox { + margin: 0; + } +} + +.login-header { + margin-top: 0; + margin-bottom: 20px; + font-size: 2em; +} + +.login-footer { + @extend .modal-footer; + + padding-left: 0; + padding-right: 0; + + .btn { + margin-right: 0; + } +} + +.modal-body { + margin-left: 20px; + margin-right: 20px; +} + +.modal-body-with-scroll { + max-height: 420px; + overflow-y: auto; + margin: 0; +} + +.greedy { + min-height: 100%; + height: #{"calc(100vh - 270px)"}; +} + +.signin-greedy { + height: #{"calc(100vh - 300px)"}; +} + +@media (min-width: 768px) { + .navbar-nav > li > a { + padding: 0 15px; + } +} + +.details-row { + padding: 0 5px; +} + +.details-row, .settings-row { + display: block; + margin: 10px 0; + + [class*="col-"] { + display: inline-block; + vertical-align: middle; + float: none; + } + + input[type="checkbox"] { + line-height: 20px; + margin-right: 5px; + } + + .checkbox label { + line-height: 20px !important; + vertical-align: middle; + } +} + +.group-section { + margin-top: 20px; +} + +.details-row:first-child { + margin-top: 0; + + .group-section { + margin-top: 10px; + } +} + +.details-row:last-child { + margin-bottom: 0; +} + +.settings-row:first-child { + margin-top: 0; + + .group-section { + margin-top: 0; + } +} + +.settings-row:last-child { + margin-bottom: 0; +} + +button, .btn { + margin-right: 5px; +} + +i.btn { + margin-right: 0; +} + +.btn { + padding: 3px 6px; + + :focus { + //outline: none; + //border: 1px solid $btn-default-border; + } +} + +.btn-group.pull-right { + margin-right: 0; +} + +.btn-group { + margin-right: 5px; + + > button, a.btn { + margin-right: 0; + } + + button.btn + .btn { + margin-left: 0; + } + + > .btn + .dropdown-toggle { + margin-right: 0; + padding: 3px 6px; + border-left-width: 0; + } +} + +h1, +h2, +h3 { + user-select: none; + font-weight: normal; + /* Makes the vertical size of the text the same for all fonts. */ + line-height: 1; +} + +h3 { + font-size: 1.2em; + margin-top: 0; + margin-bottom: 1.5em; +} + +.base-control { + text-align: left; + padding: 3px 3px; + height: $input-height; +} + +.sql-name-input { + @extend .form-control; + + width: auto; +} + +.form-control { + @extend .base-control; + + display: inline-block; + + button { + text-align: left; + } +} + +button.form-control { + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.theme-line .notebook-header { + border-color: $gray-lighter; + + h1 { + padding: 0; + margin: 0; + + height: 40px; + + label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-top: 5px; + } + + .btn-group { + margin-top: -5px; + margin-left: 5px; + } + + > i.btn { + float: right; + line-height: 30px; + } + + input { + font-size: 22px; + height: 35px; + } + + a.dropdown-toggle { + font-size: $font-size-base; + margin-right: 5px; + } + } +} + +.theme-line .sql-notebooks { + li.custom > a { + color: $brand-info; + font-weight: bold; + } + + li.custom > a:hover { + color: darken($brand-info, 15%); + } +} + +.theme-line .paragraphs { + .panel-group .panel + .panel { + margin-top: 30px; + } + + .btn-group { + margin-right: 0; + } + + .sql-editor { + padding: 5px 0; + + .ace_cursor { + opacity: 1; + } + + .ace_hidden-cursors { + opacity: 1; + } + + .ace_gutter-cell, .ace_folding-enabled > .ace_gutter-cell { + padding-right: 5px; + } + } + + .sql-controls { + margin: 10px 0; + padding: 0 10px; + } + + .sql-table-total { + padding: 0 10px; + + label, b { + display: inline-block; + + padding-top: 5px; + + height: 27px; + } + + margin-bottom: 10px; + } + + .sql-table { + height: 400px; + } + + table thead { + background-color: white; + } + + .wrong-caches-filter { + text-align: center; + color: $ignite-placeholder-color; + height: 65px; + line-height: 65px; + } + + .empty-caches { + text-align: center; + color: $ignite-placeholder-color; + height: 55px; + line-height: 55px; + } + + .sql-error-result { + padding: 10px 0; + + text-align: center; + color: $brand-primary; + + border-top: 1px solid $ignite-border-color; + } + + .sql-empty-result { + margin-top: 10px; + margin-bottom: 10px; + text-align: center; + color: $ignite-placeholder-color; + } + + .sql-next { + float: right; + + .disabled { + cursor: default; + text-decoration: none; + } + + a { + margin-right: 5px; + margin-bottom: 5px; + } + + i { + margin-top: 3px; + margin-right: 10px; + } + } +} + +.theme-line .panel-heading { + padding: 5px 10px; + margin: 0; + cursor: pointer; + font-size: $font-size-large; + line-height: 24px; + + label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: calc(100% - 85px); + cursor: pointer; + } + + .btn-group { + vertical-align:top; + margin-left: 10px; + + i { line-height: 18px; } + } + + > i { + vertical-align: top; + line-height: 26px; + height: 26px; + } + + .fa { + line-height: 26px; + } + + .fa-floppy-o { + float: right; + } + + .fa-chevron-circle-right, .fa-chevron-circle-down { + font-size: $font-size-base; + color: inherit; + float: left; + } + + .fa-undo { + padding: 1px 6px; + + font-size: 16px; + } + + .fa-undo:hover { + padding: 0 5px; + + border-radius: 5px; + border: thin dotted $ignite-darck-border-color; + } +} + +.theme-line .panel-heading:hover { + text-decoration: underline; +} + +.theme-line .panel-body { + padding: 20px; +} + +.theme-line .main-content a.customize { + margin-left: 5px; +} + +.theme-line .panel-collapse { + margin: 0; +} + +.theme-line table.links { + table-layout: fixed; + border-collapse: collapse; + + width: 100%; + + label.placeholder { + text-align: center; + color: $ignite-placeholder-color; + width: 100%; + } + + input[type="text"] { + font-weight: normal; + } + + input[type="radio"] { + margin-left: 1px; + margin-right: 5px; + } + + tbody { + border-left: 10px solid transparent; + } + + tbody td:first-child { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + tfoot > tr > td { + padding: 0; + + .pagination { + margin: 10px 0; + + > .active > a { + border-color: $table-border-color; + background-color: $gray-lighter; + } + } + } +} + +.theme-line table.links-edit { + @extend table.links; + + margin-top: 0; + margin-bottom: 5px; + + label { + line-height: $input-height; + } + + td { + padding-left: 0; + } +} + +.theme-line table.links-edit-sub { + @extend table.links-edit; + + margin-top: 0; + margin-bottom: 0; +} + +.theme-line table.links-edit-details { + @extend table.links; + + margin-bottom: 10px; + + label { + line-height: $input-height; + color: $ignite-header-color; + } + + td { + padding: 0; + + .input-tip { + padding: 0; + } + } +} + +.theme-line table.admin { + tr:hover { + cursor: default; + } + + thead { + .pagination { + margin: 0; + } + } + + thead > tr th.header { + padding: 0 0 10px; + + div { + padding: 0; + } + + input[type="text"] { + font-weight: normal; + } + } + + margin-bottom: 10px; + + label { + line-height: $input-height; + color: $ignite-header-color; + } + + thead > tr th, td { + padding: 10px 10px; + + .input-tip { + padding: 0; + } + } + + tfoot > tr > td { + padding: 0; + } + + .pagination { + margin: 10px 0; + font-weight: normal; + + > .active > a { + border-color: $table-border-color; + background-color: $gray-lighter; + } + } +} + +.admin-summary { + padding-bottom: 10px; +} + +.import-domain-model-wizard-page { + margin: 15px; +} + +.scrollable-y { + overflow-x: hidden; + overflow-y: auto; +} + +.theme-line table.metadata { + margin-bottom: 10px; + + tr:hover { + cursor: default; + } + + thead > tr { + label { + font-weight: bold; + } + + input[type="checkbox"] { + cursor: pointer; + } + } + + thead > tr th.header { + padding: 0 0 10px; + + .pull-right { + padding: 0; + } + + input[type="checkbox"] { + cursor: pointer; + } + + input[type="text"] { + font-weight: normal; + } + } + + > thead > tr > th { + padding: 5px 0 5px 5px !important; + } + + tbody > tr > td { + padding: 0; + } +} + +.td-ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.table-modal-striped { + width: 100%; + + > tbody > tr { + border-bottom: 2px solid $ignite-border-color; + + input[type="checkbox"] { + cursor: pointer; + } + } + + > tbody > tr > td { + padding: 5px 0 5px 5px !important; + } +} + +.theme-line table.sql-results { + margin: 0; + + td { + padding: 3px 6px; + } + + > thead > tr > td { + padding: 3px 0; + } + + thead > tr > th { + padding: 3px 6px; + + line-height: $input-height; + } + + tfoot > tr > td { + padding: 0; + + .pagination { + margin: 10px 0 0 0; + + > .active > a { + border-color: $table-border-color; + background-color: $gray-lighter; + } + } + } +} + +.affix { + z-index: 910; + background-color: white; + + hr { + margin: 0; + } +} + +.affix.padding-top-dflt { + hr { + margin-top: 10px; + } +} + +.affix + .bs-affix-fix { + height: 78px; +} + +.panel-details { + margin-top: 5px; + padding: 10px 5px; + + border-radius: 5px; + border: thin dotted $ignite-border-color; +} + +.group { + border-radius: 5px; + border: thin dotted $ignite-border-color; + + text-align: left; + + hr { + margin: 7px 0; + } +} + +.group-legend { + margin: -10px 5px 0 10px; + overflow: visible; + position: relative; + + label { + padding: 0 5px; + background: white; + } +} + +.group-legend-btn { + background: white; + float: right; + line-height: 20px; + padding: 0 5px 0 5px; +} + +.group-content { + margin: 10px; + + table { + width: 100%; + } +} + +.group-content-empty { + color: $input-color-placeholder; + + padding: 10px 0; + position: relative; + + text-align: center; +} + +.content-not-available { + min-height: 28px; + + margin-right: 20px; + + border-radius: 5px; + border: thin dotted $ignite-border-color; + + padding: 0; + + color: $input-color-placeholder; + display: table; + width: 100%; + height: 26px; + + label { + display: table-cell; + text-align: center; + vertical-align: middle; + } +} + +.tooltip > .tooltip-inner { + text-align: left; + border: solid 1px #ccc; +} + +.popover-footer { + margin: 0; // reset heading margin + padding: 8px 14px; + font-size: $font-size-base; + color: $input-color-placeholder; + background-color: $popover-title-bg; + border-top: 1px solid darken($popover-title-bg, 5%); + border-radius: 0 0 ($border-radius-large - 1) ($border-radius-large - 1); +} + +.popover-content { + padding: 5px; +} + +.popover:focus { + outline: none; + border: 1px solid $btn-default-border; +} + +.theme-line .popover.settings { + .close { + position: absolute; + top: 5px; + right: 5px; + } +} + +.theme-line .popover.cache-metadata { + @extend .popover.settings; + + z-index: 1030; + min-width: 305px; + max-width: 450px; + + .popover-title { + color: black; + + line-height: 27px; + + padding: 3px 5px 3px 10px; + + white-space: nowrap; + overflow: hidden; + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + + .close { + float: right; + top: 0; + right: 0; + position: relative; + margin-left: 10px; + line-height: 27px; + } + } + + > .popover-content { + overflow: auto; + + white-space: nowrap; + + min-height: 400px; + max-height: 400px; + + .content-empty { + display: block; + text-align: center; + line-height: 380px; + + color: $input-color-placeholder; + } + } + + .clickable { cursor: pointer; } +} + +.theme-line .popover.summary-project-structure { + @extend .popover.settings; + + z-index: 1030; + min-width: 305px; + + .popover-title { + color: black; + + line-height: 27px; + + padding: 3px 5px 3px 10px; + + white-space: nowrap; + overflow: hidden; + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + + .close { + float: right; + top: 0; + right: 0; + position: relative; + margin-left: 10px; + line-height: 27px; + } + } + + > .popover-content { + overflow: auto; + + white-space: nowrap; + + min-height: 300px; + max-height: 300px; + } +} + +.theme-line .popover.validation-error { + max-width: 400px; + color: $brand-primary; + background: white; + border: 1px solid $brand-primary; + + &.right > .arrow { + border-right-color: $brand-primary; + } + + .close { + vertical-align: middle; + } +} + +label { + font-weight: normal; + margin-bottom: 0; +} + +.form-horizontal .checkbox { + padding-top: 0; + min-height: 0; +} + +.input-tip { + display: block; + overflow: hidden; + position: relative; +} + +.labelHeader { + font-weight: bold; + text-transform: capitalize; +} + +.labelField { + float: left; + margin-right: 5px; +} + +.labelFormField { + float: left; + line-height: $input-height; +} + +.labelLogin { + margin-right: 10px; +} + +.form-horizontal .form-group { + margin: 0; +} + +.form-horizontal .has-feedback .form-control-feedback { + right: 0; +} + +.tipField { + float: right; + line-height: $input-height; + margin-left: 5px; +} + +.tipLabel { + font-size: $font-size-base; + margin-left: 5px; +} + +.fieldSep { + float: right; + line-height: $input-height; + margin: 0 5px; +} + +.fieldButton { + float: right; + margin-left: 5px; + margin-right: 0; +} + +.fa { + cursor: pointer; +} + +.fa-cursor-default { + cursor: default !important; +} + +.fa-remove { + color: $brand-primary; +} + +.fa-chevron-circle-down { + color: $brand-primary; + margin-right: 5px; +} + +.fa-chevron-circle-right { + color: $brand-primary; + margin-right: 5px; +} + +.fa-question-circle { + cursor: default; +} + +label.required:after { + color: $brand-primary; + content: ' *'; + display: inline; +} + +.blank { + visibility: hidden; +} + +.alert { + outline: 0; + padding: 10px; + position: fixed; + z-index: 1050; + margin: 20px; + + &.top-right { + top: 60px; + right: 0; + + .close { + padding-left: 10px; + } + } + + .alert-icon { + padding-right: 10px; + font-size: 16px; + } + + .alert-title { + color: $text-color; + } + + .close { + margin-right: 0; + line-height: 19px; + } +} + +.summary-tabs { + margin-top: 0.65em; +} + +.summary-tab { + img { + margin-right: 5px; + height: 16px; + width: 16px; + float: left; + } +} + +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type="number"] { + -moz-appearance: textfield; +} + +input.ng-dirty.ng-invalid, button.ng-dirty.ng-invalid { + border-color: $ignite-invalid-color; + + :focus { + border-color: $ignite-invalid-color; + } +} + +.form-control-feedback { + display: inline-block; + color: $brand-primary; + line-height: $input-height; + pointer-events: initial; +} + +.theme-line .nav-tabs > li > a { + padding: 5px 5px; + color: $ignite-header-color; +} + +.viewedUser { + text-align: center; + background-color: $brand-warning; +} + +a { + cursor: pointer; +} + +.st-sort-ascent:after { + content: '\25B2'; +} + +.st-sort-descent:after { + content: '\25BC'; +} + +th[st-sort] { + cursor: pointer; +} + +.panel { + margin-bottom: 0; +} + +.panel-group { + margin-bottom: 0; +} + +.panel-group .panel + .panel { + margin-top: 20px; +} + +.section { + margin-top: 20px; +} + +.section-top { + width: 100%; + margin-top: 10px; + margin-bottom: 20px; +} + +.advanced-options { + @extend .section; + margin-bottom: 20px; + + i { + font-size: 16px; + } +} + +.modal-advanced-options { + @extend .advanced-options; + margin-top: 10px; + margin-bottom: 10px; +} + +.margin-left-dflt { + margin-left: 10px; +} + +.margin-top-dflt { + margin-top: 10px; +} + +.margin-top-dflt-2x { + margin-top: 20px; +} + +.margin-bottom-dflt { + margin-bottom: 10px; +} + +.margin-dflt { + margin-top: 10px; + margin-bottom: 10px; +} + +.padding-top-dflt { + padding-top: 10px; +} + +.padding-left-dflt { + padding-left: 10px; +} + +.padding-bottom-dflt { + padding-bottom: 10px; +} + +.padding-dflt { + padding-top: 10px; + padding-bottom: 10px; +} + +.agent-download { + padding: 10px 10px 10px 20px; +} + +.ace_content { + padding-left: 5px; +} + +.ace_hidden-cursors { + opacity: 0; +} + +.ace_cursor { + opacity: 0; +} + +.ace_editor { + margin: 10px 5px 10px 0; + + .ace_gutter { + background: transparent !important; + border: 1px $ignite-border-color; + border-right-style: solid; + } + + .ace_gutter-cell, .ace_folding-enabled > .ace_gutter-cell { + padding-left: 0.65em; + } +} + +.preview-highlight-1 { + position: absolute; + background-color: #f7faff; + z-index: 20; +} + +.preview-highlight-2 { + position: absolute; + background-color: #f0f6ff; + z-index: 21; +} + +.preview-highlight-3 { + position: absolute; + background-color: #e8f2ff; + z-index: 22; +} + +.preview-highlight-4 { + position: absolute; + background-color: #e1eeff; + z-index: 23; +} + +.preview-highlight-5 { + position: absolute; + background-color: #DAEAFF; + z-index: 24; +} + +.preview-highlight-6 { + position: absolute; + background-color: #D2E5FF; + z-index: 25; +} + +.preview-highlight-7 { + position: absolute; + background-color: #CBE1FF; + z-index: 26; +} + +.preview-highlight-8 { + position: absolute; + background-color: #C3DDFF; + z-index: 27; +} + +.preview-highlight-9 { + position: absolute; + background-color: #BCD9FF; + z-index: 28; +} + +.preview-highlight-10 { + position: absolute; + background-color: #B5D5FF; + z-index: 29; +} + +.preview-panel { + min-height: 28px; + + margin-left: 20px; + + border-radius: 5px; + border: thin dotted $ignite-border-color; + + padding: 0; +} + +.preview-legend { + top: -10px; + right: 20px; + position: absolute; + z-index: 900; + + a { + background-color: white; + margin-left: 5px; + font-size: 0.9em; + } + + .inactive { + color: $input-color-placeholder; + } +} + +.preview-content-empty { + color: $input-color-placeholder; + display: table; + width: 100%; + height: 26px; + + label { + display: table-cell; + text-align: center; + vertical-align: middle; + } +} + +.chart-settings-link { + padding-left: 10px; + line-height: $input-height; + + label, button { + margin-left: 5px; + margin-right: 0; + } + + button.select-manual-caret { + padding-right: 3px; + + .caret { margin-left: 3px; } + } + + a, i { + font-size: $font-size-base; + color: $link-color !important; + margin-right: 5px; + } + + div { + margin-left: 20px; + display: inline-block; + } +} + +.chart-settings { + margin: 10px 5px 5px 5px !important; +} + +.chart-settings-columns-list { + border: 1px solid $ignite-border-color; + list-style: none; + margin-bottom: 10px; + min-height: 30px; + max-height: 200px; + padding: 5px; + + overflow: auto; + + & > li { + float: left; + } + + li:nth-child(even) { + margin-right: 0; + } + + .fa-close { + margin-left: 10px; + } +} + +.btn-chart-column { + border-radius: 3px; + font-size: 12px; + margin: 3px 3px; + padding: 1px 5px; + line-height: 1.5; + cursor: default; +} + +.btn-chart-column-movable { + @extend .btn-chart-column; + cursor: move; +} + +.btn-chart-column-agg-fx { + border: 0; + margin: 0 0 0 10px; +} + +.dw-loading { + min-height: 100px; +} + +.dw-loading > .dw-loading-body > .dw-loading-text { + left: -50%; +} + +.dw-loading.dw-loading-overlay { + z-index: 1030; +} + +.modal { + .dw-loading.dw-loading-overlay { + z-index: 9999; + } + + .dw-loading-body { + left: 10%; + } +} + +.panel-tip-container { + display: inline-block; +} + +button.dropdown-toggle { + margin-right: 5px; +} + +button.select-toggle { + position: relative; + padding-right: 15px; +} + +button.select-toggle::after { + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-left: 0.3em solid transparent; + position: absolute; + right: 5px; + top: 50%; + vertical-align: middle; +} + +// Prevent scroll bars from being hidden for OS X. +::-webkit-scrollbar { + -webkit-appearance: none; +} + +::-webkit-scrollbar:vertical { + width: 10px; +} + +::-webkit-scrollbar:horizontal { + height: 10px; +} + +::-webkit-scrollbar-thumb { + border-radius: 8px; + border: 2px solid white; /* should match background, can't be transparent */ + background-color: rgba(0, 0, 0, .5); +} + +::-webkit-scrollbar-track { + background-color: white; + border-radius: 8px; +} + +treecontrol.tree-classic { + > ul > li { + padding: 0; + } + + li { + padding-left: 15px; + } + + li.tree-expanded i.tree-branch-head.fa, li.tree-collapsed i.tree-branch-head.fa, li.tree-leaf i.tree-branch-head.fa, .tree-label i.fa { + background: none no-repeat; + padding: 1px 5px 1px 1px; + } + + li.tree-leaf i.tree-leaf-head { + background: none no-repeat !important; + padding: 0 !important; + } + + li .tree-selected { + background-color: white; + font-weight: normal; + } + + span { + margin-right: 10px; + } +} + +.docs-content { + .affix { + border-bottom: 1px solid $gray-lighter; + } + + min-height: 100px; +} + +.carousel-caption { + position: relative; + left: auto; + right: auto; + + margin-top: 10px; + + h3 { + margin-bottom: 10px; + } +} + +.carousel-control { + font-size: 20px; + z-index: 16; + + // Toggles + .fa-chevron-left,.fa-chevron-right { + position: absolute; + bottom: 28px; + margin-top: -10px; + z-index: 16; + display: inline-block; + margin-left: -10px; + } + + .fa-chevron-left { + left: 90%; + margin-left: -10px; + } + + .fa-chevron-right { + right: 90%; + margin-right: -10px; + } +} + +.carousel-control.left { + background-image: none; +} + +.carousel-control.right { + background-image: none; +} + +.getting-started-puzzle { + margin-left: 20px; +} + +.getting-started { + margin: 15px 15px 300px; +} + +.getting-started-demo { + color: $brand-info; +} + +.home-panel { + border-radius: 5px; + border: thin dotted $panel-default-border; + background-color: $panel-default-heading-bg; + + margin-top: 20px; + padding: 10px; +} + +.home { + min-height: 880px; + padding: 20px; + + @media(min-width: 992px) { + min-height: 450px; + } +} + +.additional-filter { + input[type="checkbox"] { + position: absolute; + margin-top: 8px; + } + + a { + font-weight: normal; + padding-left: 20px; + float: none; + } +} + +.grid { + .ui-grid-header-cell .ui-grid-cell-contents { + text-align: center; + + > span:not(.ui-grid-header-cell-label) { + position: absolute; + right: -3px; + } + } + + .ui-grid-cell .ui-grid-cell-contents { + text-align: center; + + > i.fa { + cursor: default; + } + } + + .ui-grid-column-menu-button { + right: -3px; + } + + .ui-grid-menu-button { + margin-top: -1px; + } + + .ui-grid-column-menu-button-last-col { + margin-right: 0 + } + + .no-rows { + .center-container { + background: white; + + .centered > div { + display: inline-block; + padding: 10px; + + opacity: 1; + + background-color: #f5f5f5; + border-radius: 6px; + border: 1px solid $ignite-darck-border-color; + } + } + } +} + +.cell-right .ui-grid-cell-contents { + text-align: right !important; +} + +.cell-left .ui-grid-cell-contents { + text-align: left !important; +} + +.grid.ui-grid { + border-left-width: 0; + border-right-width: 0; + border-bottom-width: 0; +} + +.summary-tabs { + .nav-tabs > li:first-child, + .nav-tabs > li:first-child.active { + & > a, + & > a:focus, + & > a:hover { + border-left: none; + border-top-left-radius: 0; + } + } +} + +.ribbon-wrapper { + width: 150px; + height: 150px; + position: absolute; + overflow: hidden; + top: 0; + z-index: 1001; + pointer-events: none; +} + +.ribbon-wrapper.right { + right: 0; +} + +.ribbon { + position: absolute; + top: 42px; + width: 200px; + padding: 1px 0; + color: $btn-primary-color; + background: $btn-primary-border; + + -moz-box-shadow: 0 0 10px rgba(0,0,0,0.5); + -webkit-box-shadow: 0 0 10px rgba(0,0,0,0.5); + box-shadow: 0 0 10px rgba(0,0,0,0.5); + + right: -42px; + -moz-transform: rotate(45deg); + -webkit-transform: rotate(45deg); + -o-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); + + > label { + display: block; + padding: 1px 0; + height: 24px; + line-height: 18px; + + text-align: center; + text-decoration: none; + font-family: 'Roboto Slab', sans-serif; + font-size: 20px; + font-weight: 500; + + border: 1px solid rgba(255,255,255,0.3); + + -moz-text-shadow: 0 0 10px rgba(0,0,0,0.31); + -webkit-text-shadow: 0 0 10px rgba(0,0,0,0.31); + text-shadow: 0 0 10px rgba(0,0,0,0.31); + } +} + +html,body,.splash-screen { + width: 100%; + height: 100%; +} + +.splash { + position: fixed; + bottom: 0; + left: 0; + right: 0; + top: 0; + opacity: 1; + background-color: white; + z-index: 99999; + + .splash-wrapper { + display: inline-block; + vertical-align: middle; + position: relative; + width: 100%; + } + + .splash-wellcome { + font-size: 18px; + margin: 20px 0; + text-align: center; + } +} + +.splash:before { + content: ''; + display: inline-block; + height: 100%; + vertical-align: middle; +} + +.spinner { + margin: 0 auto; + width: 100px; + text-align: center; + + > div { + width: 18px; + height: 18px; + margin: 0 5px; + border-radius: 100%; + display: inline-block; + -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; + animation: sk-bouncedelay 1.4s infinite ease-in-out both; + background-color: $brand-primary; + } + + .bounce1 { + -webkit-animation-delay: -0.32s; + animation-delay: -0.32s; + } + + .bounce2 { + -webkit-animation-delay: -0.16s; + animation-delay: -0.16s; + } +} + +@-webkit-keyframes sk-bouncedelay { + 0%, 80%, 100% { + -webkit-transform: scale(0) + } + 40% { + -webkit-transform: scale(1.0) + } +} + +@keyframes sk-bouncedelay { + 0%, 80%, 100% { + -webkit-transform: scale(0); + transform: scale(0); + } + 40% { + -webkit-transform: scale(1.0); + transform: scale(1.0); + } +} + +[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { + display: none !important; +} + +.nvd3 .nv-axis .nv-axisMaxMin text { + font-weight: normal; /* Here the text can be modified*/ +}
http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/public/stylesheets/variables.scss ---------------------------------------------------------------------- diff --git a/modules/web-console/src/main/js/public/stylesheets/variables.scss b/modules/web-console/src/main/js/public/stylesheets/variables.scss new file mode 100644 index 0000000..8500eac --- /dev/null +++ b/modules/web-console/src/main/js/public/stylesheets/variables.scss @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "bootstrap-variables"; + +$logo-path: "/images/logo.png"; +$input-height: 28px; +$ignite-placeholder-color: #999999; +$ignite-border-color: #ddd; +$ignite-darck-border-color: #aaa; +$ignite-border-bottom-color: $brand-primary; +$ignite-background-color: #fff; +$ignite-header-color: #555; +$ignite-invalid-color: $brand-primary; http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve.js ---------------------------------------------------------------------- diff --git a/modules/web-console/src/main/js/serve.js b/modules/web-console/src/main/js/serve.js new file mode 100644 index 0000000..891855c --- /dev/null +++ b/modules/web-console/src/main/js/serve.js @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const http = require('http'), + https = require('https'), + path = require('path'); + +/** + * Event listener for HTTP server "error" event. + */ +const _onError = (port, error) => { + if (error.syscall !== 'listen') + throw error; + + var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; + + // Handle specific listen errors with friendly messages. + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + + break; + default: + throw error; + } +}; + +/** + * Event listener for HTTP server "listening" event. + */ +const _onListening = (addr) => { + var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; + + console.log('Start listening on ' + bind); +}; + +const igniteModules = (process.env.IGNITE_MODULES && path.relative(__dirname, process.env.IGNITE_MODULES)) || './ignite_modules'; + +const fireUp = require('fire-up').newInjector({ + basePath: __dirname, + modules: [ + './serve/**/*.js', + `${igniteModules}/**/*.js` + ] +}); + +Promise.all([fireUp('settings'), fireUp('app'), fireUp('agent-manager'), fireUp('browser-manager')]) + .then((values) => { + const settings = values[0]; + const app = values[1]; + const agentMgr = values[2]; + const browserMgr = values[3]; + + // Start rest server. + const server = settings.server.SSLOptions + ? https.createServer(settings.server.SSLOptions) : http.createServer(); + + server.listen(settings.server.port); + server.on('error', _onError.bind(null, settings.server.port)); + server.on('listening', _onListening.bind(null, server.address())); + + app.listen(server); + browserMgr.attach(server); + + // Start legacy agent server for reject connection with message. + if (settings.agent.legacyPort) { + const agentLegacySrv = settings.agent.SSLOptions + ? https.createServer(settings.agent.SSLOptions) : http.createServer(); + + agentLegacySrv.listen(settings.agent.legacyPort); + agentLegacySrv.on('error', _onError.bind(null, settings.agent.legacyPort)); + agentLegacySrv.on('listening', _onListening.bind(null, agentLegacySrv.address())); + + agentMgr.attachLegacy(agentLegacySrv); + } + + // Start agent server. + const agentServer = settings.agent.SSLOptions + ? https.createServer(settings.agent.SSLOptions) : http.createServer(); + + agentServer.listen(settings.agent.port); + agentServer.on('error', _onError.bind(null, settings.agent.port)); + agentServer.on('listening', _onListening.bind(null, agentServer.address())); + + agentMgr.attach(agentServer); + + // Used for automated test. + if (process.send) + process.send('running'); + }).catch((err) => { + console.error(err); + + process.exit(1); + }); http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/agent.js ---------------------------------------------------------------------- diff --git a/modules/web-console/src/main/js/serve/agent.js b/modules/web-console/src/main/js/serve/agent.js new file mode 100644 index 0000000..78dd66f --- /dev/null +++ b/modules/web-console/src/main/js/serve/agent.js @@ -0,0 +1,601 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +/** + * Module interaction with agents. + */ +module.exports = { + implements: 'agent-manager', + inject: ['require(lodash)', 'require(ws)', 'require(fs)', 'require(path)', 'require(jszip)', 'require(socket.io)', 'settings', 'mongo'] +}; + +/** + * @param _ + * @param fs + * @param ws + * @param path + * @param JSZip + * @param socketio + * @param settings + * @param mongo + * @returns {AgentManager} + */ +module.exports.factory = function(_, ws, fs, path, JSZip, socketio, settings, mongo) { + /** + * + */ + class Command { + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} name Command name. + */ + constructor(demo, name) { + this._demo = demo; + + /** + * Command name. + * @type {String} + */ + this._name = name; + + /** + * Command parameters. + * @type {Array.<String>} + */ + this._params = []; + } + + /** + * Add parameter to command. + * @param {string} key Parameter key. + * @param {Object} value Parameter value. + * @returns {Command} + */ + addParam(key, value) { + this._params.push({key, value}); + + return this; + } + } + + /** + * Connected agent descriptor. + */ + class Agent { + /** + * @param {socketIo.Socket} socket - Agent socket for interaction. + */ + constructor(socket) { + /** + * Agent socket for interaction. + * + * @type {socketIo.Socket} + * @private + */ + this._socket = socket; + } + + /** + * Send message to agent. + * + * @this {Agent} + * @param {String} event Command name. + * @param {Object} data Command params. + * @param {Function} [callback] on finish + */ + _emit(event, data, callback) { + if (!this._socket.connected) { + if (callback) + callback('org.apache.ignite.agent.AgentException: Connection is closed'); + + return; + } + + this._socket.emit(event, data, callback); + } + + /** + * Send message to agent. + * + * @param {String} event - Event name. + * @param {Object?} data - Transmitted data. + * @returns {Promise} + */ + executeAgent(event, data) { + return new Promise((resolve, reject) => + this._emit(event, data, (error, res) => { + if (error) + return reject(error); + + resolve(res); + }) + ); + } + + /** + * Execute rest request on node. + * + * @param {Command} cmd - REST command. + * @return {Promise} + */ + executeRest(cmd) { + const params = {cmd: cmd._name}; + + for (const param of cmd._params) + params[param.key] = param.value; + + return new Promise((resolve, reject) => { + this._emit('node:rest', {uri: 'ignite', params, demo: cmd._demo, method: 'GET'}, (error, res) => { + if (error) + return reject(new Error(error)); + + error = res.error; + + const code = res.code; + + if (code === 401) + return reject(new Error('Agent is failed to authenticate in grid. Please check agent\'s login and password or node port.')); + + if (code !== 200) + return reject(new Error(error || 'Failed connect to node and execute REST command.')); + + try { + const msg = JSON.parse(res.data); + + if (msg.successStatus === 0) + return resolve(msg.response); + + if (msg.successStatus === 2) + return reject(new Error('Agent is failed to authenticate in grid. Please check agent\'s login and password or node port.')); + + reject(new Error(msg.error)); + } + catch (e) { + return reject(e); + } + }); + }); + } + + /** + * @param {String} driverPath + * @param {String} driverClass + * @param {String} url + * @param {Object} info + * @returns {Promise} Promise on list of tables (see org.apache.ignite.schema.parser.DbTable java class) + */ + metadataSchemas(driverPath, driverClass, url, info) { + return this.executeAgent('schemaImport:schemas', {driverPath, driverClass, url, info}); + } + + /** + * @param {String} driverPath + * @param {String} driverClass + * @param {String} url + * @param {Object} info + * @param {Array} schemas + * @param {Boolean} tablesOnly + * @returns {Promise} Promise on list of tables (see org.apache.ignite.schema.parser.DbTable java class) + */ + metadataTables(driverPath, driverClass, url, info, schemas, tablesOnly) { + return this.executeAgent('schemaImport:metadata', {driverPath, driverClass, url, info, schemas, tablesOnly}); + } + + /** + * @returns {Promise} Promise on list of jars from driver folder. + */ + availableDrivers() { + return this.executeAgent('schemaImport:drivers'); + } + + /** + * + * @param {Boolean} demo Is need run command on demo node. + * @param {Boolean} attr Get attributes, if this parameter has value true. Default value: true. + * @param {Boolean} mtr Get metrics, if this parameter has value true. Default value: false. + * @returns {Promise} + */ + topology(demo, attr, mtr) { + const cmd = new Command(demo, 'top') + .addParam('attr', attr !== false) + .addParam('mtr', !!mtr); + + return this.executeRest(cmd); + } + + /** + * + * @param {Boolean} demo Is need run command on demo node. + * @param {String} cacheName Cache name. + * @param {String} query Query. + * @param {int} pageSize Page size. + * @returns {Promise} + */ + fieldsQuery(demo, cacheName, query, pageSize) { + const cmd = new Command(demo, 'qryfldexe') + .addParam('cacheName', cacheName) + .addParam('qry', query) + .addParam('pageSize', pageSize); + + return this.executeRest(cmd); + } + + /** + * + * @param {Boolean} demo Is need run command on demo node. + * @param {String} cacheName Cache name. + * @param {int} pageSize Page size. + * @returns {Promise} + */ + scan(demo, cacheName, pageSize) { + const cmd = new Command(demo, 'qryscanexe') + .addParam('cacheName', cacheName) + .addParam('pageSize', pageSize); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {int} queryId Query Id. + * @param {int} pageSize Page size. + * @returns {Promise} + */ + queryFetch(demo, queryId, pageSize) { + const cmd = new Command(demo, 'qryfetch') + .addParam('qryId', queryId) + .addParam('pageSize', pageSize); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {int} queryId Query Id. + * @returns {Promise} + */ + queryClose(demo, queryId) { + const cmd = new Command(demo, 'qrycls') + .addParam('qryId', queryId); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} cacheName Cache name. + * @returns {Promise} + */ + metadata(demo, cacheName) { + const cmd = new Command(demo, 'metadata') + .addParam('cacheName', cacheName); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} evtOrderKey Event order key, unique for tab instance. + * @param {String} evtThrottleCntrKey Event throttle counter key, unique for tab instance. + * @returns {Promise} + */ + collect(demo, evtOrderKey, evtThrottleCntrKey) { + const cmd = new Command(demo, 'exe') + .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask') + .addParam('p1', '') + .addParam('p2', 'org.apache.ignite.internal.visor.node.VisorNodeDataCollectorTask') + .addParam('p3', 'org.apache.ignite.internal.visor.node.VisorNodeDataCollectorTaskArg') + .addParam('p4', true) + .addParam('p5', 'CONSOLE_' + evtOrderKey) + .addParam('p6', evtThrottleCntrKey) + .addParam('p7', 10) + .addParam('p8', false); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} nid Node id. + * @param {String} cacheName Cache name. + * @returns {Promise} + */ + cacheClear(demo, nid, cacheName) { + const cmd = new Command(demo, 'exe') + .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask') + .addParam('p1', nid) + .addParam('p2', 'org.apache.ignite.internal.visor.cache.VisorCacheClearTask') + .addParam('p3', 'java.lang.String') + .addParam('p4', cacheName); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} nid Node id. + * @param {String} cacheName Cache name. + * @returns {Promise} + */ + cacheStop(demo, nid, cacheName) { + const cmd = new Command(demo, 'exe') + .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask') + .addParam('p1', nid) + .addParam('p2', 'org.apache.ignite.internal.visor.cache.VisorCacheStopTask') + .addParam('p3', 'java.lang.String') + .addParam('p4', cacheName); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} nid Node id. + * @returns {Promise} + */ + ping(demo, nid) { + const cmd = new Command(demo, 'exe') + .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask') + .addParam('p1', 'null') + .addParam('p2', 'org.apache.ignite.internal.visor.node.VisorNodePingTask') + .addParam('p3', 'java.util.UUID') + .addParam('p4', nid); + + return this.executeRest(cmd); + } + } + + /** + * Connected agents manager. + */ + class AgentManager { + /** + * @constructor + */ + constructor() { + /** + * Connected agents by user id. + * @type {Object.<ObjectId, Array.<Agent>>} + */ + this._agents = {}; + + /** + * Connected browsers by user id. + * @type {Object.<ObjectId, Array.<Socket>>} + */ + this._browsers = {}; + + const agentArchives = fs.readdirSync(settings.agent.dists) + .filter((file) => path.extname(file) === '.zip'); + + /** + * Supported agents distribution. + * @type {Object.<String, String>} + */ + this.supportedAgents = {}; + + const jarFilter = (file) => path.extname(file) === '.jar'; + + for (const archive of agentArchives) { + const filePath = path.join(settings.agent.dists, archive); + + const zip = new JSZip(fs.readFileSync(filePath)); + + const jarPath = _.find(_.keys(zip.files), jarFilter); + + const jar = new JSZip(zip.files[jarPath].asNodeBuffer()); + + const manifest = jar.files['META-INF/MANIFEST.MF'] + .asText() + .trim() + .split(/\s*\n+\s*/) + .map((line, r) => { + r = line.split(/\s*:\s*/); + + this[r[0]] = r[1]; + + return this; + }, {})[0]; + + const ver = manifest['Implementation-Version']; + + if (ver) { + this.supportedAgents[ver] = { + fileName: archive, + filePath, + buildTime: manifest['Build-Time'] + }; + } + } + + const latest = _.head(Object.keys(this.supportedAgents).sort((a, b) => { + const aParts = a.split('.'); + const bParts = b.split('.'); + + for (let i = 0; i < aParts.length; ++i) { + if (bParts.length === i) + return 1; + + if (aParts[i] === aParts[i]) + continue; + + return aParts[i] > bParts[i] ? 1 : -1; + } + })); + + // Latest version of agent distribution. + if (latest) + this.supportedAgents.latest = this.supportedAgents[latest]; + } + + attachLegacy(server) { + const wsSrv = new ws.Server({server}); + + wsSrv.on('connection', (_wsClient) => { + _wsClient.send(JSON.stringify({ + method: 'authResult', + args: ['You are using an older version of the agent. Please reload agent archive'] + })); + }); + } + + /** + * @param {http.Server|https.Server} srv Server instance that we want to attach agent handler. + */ + attach(srv) { + if (this._server) + throw 'Agent server already started!'; + + this._server = srv; + + /** + * @type {socketIo.Server} + */ + this._socket = socketio(this._server); + + this._socket.on('connection', (socket) => { + socket.on('agent:auth', (data, cb) => { + if (!_.isEmpty(this.supportedAgents)) { + const ver = data.ver; + const bt = data.bt; + + if (_.isEmpty(ver) || _.isEmpty(bt) || _.isEmpty(this.supportedAgents[ver]) || + this.supportedAgents[ver].buildTime > bt) + return cb('You are using an older version of the agent. Please reload agent archive'); + } + + mongo.Account.findOne({token: data.token}, (err, account) => { + // TODO IGNITE-1379 send error to web master. + if (err) + cb('Failed to authorize user'); + else if (!account) + cb('Invalid token, user not found'); + else { + const agent = new Agent(socket); + + socket.on('disconnect', () => { + this._removeAgent(account._id, agent); + }); + + this._addAgent(account._id, agent); + + cb(); + } + }); + }); + }); + } + + /** + * @param {ObjectId} userId + * @param {Socket} user + * @returns {int} connected agent count. + */ + addAgentListener(userId, user) { + let users = this._browsers[userId]; + + if (!users) + this._browsers[userId] = users = []; + + users.push(user); + + const agents = this._agents[userId]; + + return agents ? agents.length : 0; + } + + /** + * @param {ObjectId} userId + * @param {Socket} user + * @returns {int} connected agent count. + */ + removeAgentListener(userId, user) { + const users = this._browsers[userId]; + + _.remove(users, (_user) => _user === user); + } + + /** + * @param {ObjectId} userId + * @returns {Promise.<Agent>} + */ + findAgent(userId) { + if (!this._server) + return Promise.reject(new Error('Agent server not started yet!')); + + const agents = this._agents[userId]; + + if (!agents || agents.length === 0) + return Promise.reject(new Error('Failed to connect to agent')); + + return Promise.resolve(agents[0]); + } + + /** + * Close connections for all user agents. + * @param {ObjectId} userId + */ + close(userId) { + if (!this._server) + return; + + const agents = this._agents[userId]; + + this._agents[userId] = []; + + for (const agent of agents) + agent._emit('agent:close', 'Security token was changed for user'); + } + + /** + * @param userId + * @param {Agent} agent + */ + _removeAgent(userId, agent) { + const agents = this._agents[userId]; + + _.remove(agents, (_agent) => _agent === agent); + + const users = this._browsers[userId]; + + _.forEach(users, (user) => user.emit('agent:count', {count: agents.length})); + } + + /** + * @param {ObjectId} userId + * @param {Agent} agent + */ + _addAgent(userId, agent) { + let agents = this._agents[userId]; + + if (!agents) + this._agents[userId] = agents = []; + + agents.push(agent); + + const users = this._browsers[userId]; + + _.forEach(users, (user) => user.emit('agent:count', {count: agents.length})); + } + } + + return new AgentManager(); +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/agent_dists/README.txt ---------------------------------------------------------------------- diff --git a/modules/web-console/src/main/js/serve/agent_dists/README.txt b/modules/web-console/src/main/js/serve/agent_dists/README.txt new file mode 100644 index 0000000..d51bdf9 --- /dev/null +++ b/modules/web-console/src/main/js/serve/agent_dists/README.txt @@ -0,0 +1,7 @@ +Ignite Web Console +====================================== + +This is default folder for agent distributives. + +Also, you could specify custom folder in `serve/config/settings.json` + http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/app.js ---------------------------------------------------------------------- diff --git a/modules/web-console/src/main/js/serve/app.js b/modules/web-console/src/main/js/serve/app.js new file mode 100644 index 0000000..5d6b2cf --- /dev/null +++ b/modules/web-console/src/main/js/serve/app.js @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +module.exports = { + implements: 'app', + inject: ['require(express)', 'configure', 'routes'] +}; + +module.exports.factory = function(Express, configure, routes) { + return { + /** + * @param {Server} srv + */ + listen: (srv) => { + const app = new Express(); + + configure.express(app); + + routes.register(app); + + srv.addListener('request', app); + } + }; +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/browser.js ---------------------------------------------------------------------- diff --git a/modules/web-console/src/main/js/serve/browser.js b/modules/web-console/src/main/js/serve/browser.js new file mode 100644 index 0000000..837450d --- /dev/null +++ b/modules/web-console/src/main/js/serve/browser.js @@ -0,0 +1,304 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +/** + * Module interaction with browsers. + */ +module.exports = { + implements: 'browser-manager', + inject: ['require(lodash)', 'require(socket.io)', 'agent-manager', 'configure'] +}; + +module.exports.factory = (_, socketio, agentMgr, configure) => { + const _errorToJson = (err) => { + return { + message: err.message || err, + code: err.code || 1 + }; + }; + + return { + attach: (server) => { + const io = socketio(server); + + configure.socketio(io); + + io.sockets.on('connection', (socket) => { + const user = socket.client.request.user; + + const demo = socket.client.request._query.IgniteDemoMode === 'true'; + + // Return available drivers to browser. + socket.on('schemaImport:drivers', (cb) => { + agentMgr.findAgent(user._id) + .then((agent) => agent.availableDrivers()) + .then((drivers) => cb(null, drivers)) + .catch((err) => cb(_errorToJson(err))); + }); + + // Return schemas from database to browser. + socket.on('schemaImport:schemas', (preset, cb) => { + agentMgr.findAgent(user._id) + .then((agent) => { + const jdbcInfo = {user: preset.user, password: preset.password}; + + return agent.metadataSchemas(preset.jdbcDriverJar, preset.jdbcDriverClass, preset.jdbcUrl, jdbcInfo); + }) + .then((schemas) => cb(null, schemas)) + .catch((err) => cb(_errorToJson(err))); + }); + + // Return tables from database to browser. + socket.on('schemaImport:tables', (preset, cb) => { + agentMgr.findAgent(user._id) + .then((agent) => { + const jdbcInfo = {user: preset.user, password: preset.password}; + + return agent.metadataTables(preset.jdbcDriverJar, preset.jdbcDriverClass, preset.jdbcUrl, jdbcInfo, + preset.schemas, preset.tablesOnly); + }) + .then((tables) => cb(null, tables)) + .catch((err) => cb(_errorToJson(err))); + }); + + // Return topology command result from grid to browser. + socket.on('node:topology', (attr, mtr, cb) => { + agentMgr.findAgent(user._id) + .then((agent) => agent.topology(demo, attr, mtr)) + .then((clusters) => cb(null, clusters)) + .catch((err) => cb(_errorToJson(err))); + }); + + // Close query on node. + socket.on('node:query:close', (queryId, cb) => { + agentMgr.findAgent(user._id) + .then((agent) => agent.queryClose(demo, queryId)) + .then(() => cb()) + .catch((err) => cb(_errorToJson(err))); + }); + + // Execute query on node and return first page to browser. + socket.on('node:query', (cacheName, pageSize, query, cb) => { + agentMgr.findAgent(user._id) + .then((agent) => { + if (query === null) + return agent.scan(demo, cacheName, pageSize); + + return agent.fieldsQuery(demo, cacheName, query, pageSize); + }) + .then((res) => cb(null, res)) + .catch((err) => cb(_errorToJson(err))); + }); + + // Fetch next page for query and return result to browser. + socket.on('node:query:fetch', (queryId, pageSize, cb) => { + agentMgr.findAgent(user._id) + .then((agent) => agent.queryFetch(demo, queryId, pageSize)) + .then((res) => cb(null, res)) + .catch((err) => cb(_errorToJson(err))); + }); + + // Execute query on node and return full result to browser. + socket.on('node:query:getAll', (cacheName, query, cb) => { + // Set page size for query. + const pageSize = 1024; + + agentMgr.findAgent(user._id) + .then((agent) => { + const firstPage = query === null ? agent.scan(demo, cacheName, pageSize) + : agent.fieldsQuery(demo, cacheName, query, pageSize); + + const fetchResult = (acc) => { + if (acc.last) + return acc; + + return agent.queryFetch(demo, acc.queryId, pageSize) + .then((res) => { + acc.rows = acc.rows.concat(res.rows); + + acc.last = res.last; + + return fetchResult(acc); + }); + }; + + return firstPage + .then(fetchResult); + }) + .then((res) => cb(null, res)) + .catch((err) => cb(_errorToJson(err))); + }); + + // Return cache metadata from all nodes in grid. + socket.on('node:cache:metadata', (cacheName, cb) => { + agentMgr.findAgent(user._id) + .then((agent) => agent.metadata(demo, cacheName)) + .then((caches) => { + let types = []; + + const _compact = (className) => { + return className.replace('java.lang.', '').replace('java.util.', '').replace('java.sql.', ''); + }; + + const _typeMapper = (meta, typeName) => { + let fields = meta.fields[typeName]; + + let columns = []; + + for (const fieldName in fields) { + if (fields.hasOwnProperty(fieldName)) { + const fieldClass = _compact(fields[fieldName]); + + columns.push({ + type: 'field', + name: fieldName, + clazz: fieldClass, + system: fieldName === '_KEY' || fieldName === '_VAL', + cacheName: meta.cacheName, + typeName + }); + } + } + + const indexes = []; + + for (const index of meta.indexes[typeName]) { + fields = []; + + for (const field of index.fields) { + fields.push({ + type: 'index-field', + name: field, + order: index.descendings.indexOf(field) < 0, + unique: index.unique, + cacheName: meta.cacheName, + typeName + }); + } + + if (fields.length > 0) { + indexes.push({ + type: 'index', + name: index.name, + children: fields, + cacheName: meta.cacheName, + typeName + }); + } + } + + columns = _.sortBy(columns, 'name'); + + if (!_.isEmpty(indexes)) { + columns = columns.concat({ + type: 'indexes', + name: 'Indexes', + cacheName: meta.cacheName, + typeName, + children: indexes + }); + } + + return { + type: 'type', + cacheName: meta.cacheName || '', + typeName, + children: columns + }; + }; + + for (const meta of caches) { + const cacheTypes = meta.types.map(_typeMapper.bind(null, meta)); + + if (!_.isEmpty(cacheTypes)) + types = types.concat(cacheTypes); + } + + return cb(null, types); + }) + .catch((err) => cb(_errorToJson(err))); + }); + + // Fetch next page for query and return result to browser. + socket.on('node:visor:collect', (evtOrderKey, evtThrottleCntrKey, cb) => { + agentMgr.findAgent(user._id) + .then((agent) => agent.collect(demo, evtOrderKey, evtThrottleCntrKey)) + .then((data) => { + if (data.finished) + return cb(null, data.result); + + cb(_errorToJson(data.error)); + }) + .catch((err) => cb(_errorToJson(err))); + }); + + // Clear specified cache on specified node and return result to browser. + socket.on('node:cache:clear', (nid, cacheName, cb) => { + agentMgr.findAgent(user._id) + .then((agent) => agent.cacheClear(demo, nid, cacheName)) + .then((data) => { + if (data.finished) + return cb(null, data.result); + + cb(_errorToJson(data.error)); + }) + .catch((err) => cb(_errorToJson(err))); + }); + + // Stop specified cache on specified node and return result to browser. + socket.on('node:cache:stop', (nids, cacheName, cb) => { + agentMgr.findAgent(user._id) + .then((agent) => agent.cacheStop(demo, nids, cacheName)) + .then((data) => { + if (data.finished) + return cb(null, data.result); + + cb(_errorToJson(data.error)); + }) + .catch((err) => cb(_errorToJson(err))); + }); + + + // Ping node and return result to browser. + socket.on('node:ping', (nid, cb) => { + agentMgr.findAgent(user._id) + .then((agent) => agent.ping(demo, nid)) + .then((data) => { + if (data.finished) + return cb(null, data.result); + + cb(_errorToJson(data.error)); + }) + .catch((err) => cb(_errorToJson(err))); + }); + + const count = agentMgr.addAgentListener(user._id, socket); + + socket.emit('agent:count', {count}); + }); + + // Handle browser disconnect event. + io.sockets.on('disconnect', (socket) => + agentMgr.removeAgentListener(socket.client.request.user._id, socket) + ); + } + }; +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/config/settings.json.sample ---------------------------------------------------------------------- diff --git a/modules/web-console/src/main/js/serve/config/settings.json.sample b/modules/web-console/src/main/js/serve/config/settings.json.sample new file mode 100644 index 0000000..94dd9f7 --- /dev/null +++ b/modules/web-console/src/main/js/serve/config/settings.json.sample @@ -0,0 +1,26 @@ +{ + "server": { + "port": 3000, + "ssl": false, + "key": "serve/keys/test.key", + "cert": "serve/keys/test.crt", + "keyPassphrase": "password" + }, + "mongoDB": { + "url": "mongodb://localhost/console" + }, + "agent-server": { + "port": 3001, + "ssl": false, + "key": "serve/keys/test.key", + "cert": "serve/keys/test.crt", + "keyPassphrase": "password" + }, + "smtp": { + "service": "", + "username": "Apache Ignite Web Console", + "sign": "Kind regards,<br>Apache Ignite Team", + "email": "", + "password": "" + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/configure.js ---------------------------------------------------------------------- diff --git a/modules/web-console/src/main/js/serve/configure.js b/modules/web-console/src/main/js/serve/configure.js new file mode 100644 index 0000000..71f7c8a --- /dev/null +++ b/modules/web-console/src/main/js/serve/configure.js @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +/** + * Module for configuration express and websocket server. + */ +module.exports = { + implements: 'configure', + inject: ['require(morgan)', 'require(cookie-parser)', 'require(body-parser)', + 'require(express-session)', 'require(connect-mongo)', 'require(passport)', 'require(passport.socketio)', 'settings', 'mongo'] +}; + +module.exports.factory = function(logger, cookieParser, bodyParser, session, connectMongo, passport, passportSocketIo, settings, mongo) { + const _sessionStore = new (connectMongo(session))({mongooseConnection: mongo.connection}); + + return { + express: (app) => { + app.use(logger('dev', { + skip: (req, res) => res.statusCode < 400 + })); + + app.use(cookieParser(settings.sessionSecret)); + + app.use(bodyParser.json({limit: '50mb'})); + app.use(bodyParser.urlencoded({limit: '50mb', extended: true})); + + app.use(session({ + secret: settings.sessionSecret, + resave: false, + saveUninitialized: true, + cookie: { + expires: new Date(Date.now() + settings.cookieTTL), + maxAge: settings.cookieTTL + }, + store: _sessionStore + })); + + app.use(passport.initialize()); + app.use(passport.session()); + + passport.serializeUser(mongo.Account.serializeUser()); + passport.deserializeUser(mongo.Account.deserializeUser()); + + passport.use(mongo.Account.createStrategy()); + }, + socketio: (io) => { + const _onAuthorizeSuccess = (data, accept) => { + accept(null, true); + }; + + const _onAuthorizeFail = (data, message, error, accept) => { + accept(null, false); + }; + + io.use(passportSocketIo.authorize({ + cookieParser, + key: 'connect.sid', // the name of the cookie where express/connect stores its session_id + secret: settings.sessionSecret, // the session_secret to parse the cookie + store: _sessionStore, // we NEED to use a sessionstore. no memorystore please + success: _onAuthorizeSuccess, // *optional* callback on success - read more below + fail: _onAuthorizeFail // *optional* callback on fail/error - read more below + })); + } + }; +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/keys/test.crt ---------------------------------------------------------------------- diff --git a/modules/web-console/src/main/js/serve/keys/test.crt b/modules/web-console/src/main/js/serve/keys/test.crt new file mode 100644 index 0000000..50c6d5c --- /dev/null +++ b/modules/web-console/src/main/js/serve/keys/test.crt @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB6zCCAVQCCQDcAphbU6UcLjANBgkqhkiG9w0BAQsFADA6MRIwEAYDVQQDDAls +b2NhbGhvc3QxJDAiBgkqhkiG9w0BCQEWFXNldmRva2ltb3ZAYXBhY2hlLm9yZzAe +Fw0xNTA3MTQxMzAyNTNaFw0xODA2MjMxMzAyNTNaMDoxEjAQBgNVBAMMCWxvY2Fs +aG9zdDEkMCIGCSqGSIb3DQEJARYVc2V2ZG9raW1vdkBhcGFjaGUub3JnMIGfMA0G +CSqGSIb3DQEBAQUAA4GNADCBiQKBgQDP/zpJrdHqCj6lPpeFF6LQtzKef6UiyBBo +rbuOtCCgW8KMJJciluBWk2126qLt9smBN4jBpSNU3pq0r9gBMUTd/LSe7aY4D5ED +Pjp7XsypNVKeHaHbFi7KhfHy0LYxsWiNPmmHJv4dtYOp+pGK25rkXNfyJxxjgxN6 +wo34+MnZIQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAFk9XEjcdyihws+fVmdGGUFo +bVxI9YGH6agiNbU3WNF4B4VRzcPPW8z2mEo7eF9kgYmq/YzH4T8tgi/qkL/u8eZV +Wmi9bg6RThLN6/hj3wVoOFKykbDQ05FFdhIJXN5UOjPmxYM97EKqg6J0W2HAb8SG ++UekPnmAo/2HTKsLykH8 +-----END CERTIFICATE----- http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/keys/test.key ---------------------------------------------------------------------- diff --git a/modules/web-console/src/main/js/serve/keys/test.key b/modules/web-console/src/main/js/serve/keys/test.key new file mode 100644 index 0000000..1b395c0 --- /dev/null +++ b/modules/web-console/src/main/js/serve/keys/test.key @@ -0,0 +1,18 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,6798185330CE2EE2 + +sOwkmD8rvjx11l09V26dJhLhl+SyPIhyeZ3TqHXrYCATKoXlzidT+uPu1jVYtrwr +nBLA6TrIDYRrBNlEsqGZ0cSvWTIczzVW1xZKHEJo5q2vUT/W8u/Q1QQtS3P3GeKF +dEzx496rpZqwwVw59GNbuIwyYoVvQf3iEXzfhplGmLPELYIplDFOLgNuXQyXSGx6 +rwKsCxXMLsDyrA6DCz0Odf08p2HvWk/s5Ne3DFcQlqRNtIrBVGD2O0/Fp8ZZ2I4E +Yn2OIIWJff3HanOjLOWKdN8YAn5UleNmlEUdIHeS5qaQ68mabOxLkSef9qglV+sd +FHTtUq0cG6t6nhxZBziexha6v1yl/xABAHHhNPOfak+HthWxRD4N9f1yFYAeTmkn +4kwBWoSUe12XRf2pGNqhEUKN/KhDmWk85wI55i/Cu2XmNoiBFlS9BXrRYU8uVCJw +KlxjKTDWl1opCyvxTDxJnMkt44ZT445LRePKVueGIIKSUIXNQypOE+C1I0CL0N2W +Ts3m9nthquvLeMx92k7b8yW69BER5uac3SIlGCOJObQXsHgyk8wYiyd/zLKfjctG +PXieaW81UKjp+GqWpvWPz3VqnKwoyUWeVOOTviurli6kYOrHuySTMqMb6hxJctw9 +grAQTT0UPiAKWcM7InLzZnRjco+v9QLLEokjVngXPba16K/CItFY16xuGlaFLW7Y +XTc67AkL8b76HBZelMjmCsqjvSoULhuMFwTOvUMm/mSM8rMoi9asrJRLQHRMWCST +/6RENPLzPlOMnNLBujpBbn8V3/aYzEZsHMI+6S3d27WYlTJIqpabSA== +-----END RSA PRIVATE KEY-----
