You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@griffin.apache.org by gu...@apache.org on 2018/10/29 01:56:02 UTC

incubator-griffin git commit: [GRIFFIN-203] Plaintext mode" for measure creation

Repository: incubator-griffin
Updated Branches:
  refs/heads/master e4d587d95 -> bcfd0e269


[GRIFFIN-203] Plaintext mode" for measure creation

Created json/yaml measure creation flow:
![2018-10-24 21 41 44](https://user-images.githubusercontent.com/15277543/47476643-4aa37680-d7d6-11e8-9488-fe2e247617a1.gif)

Added ability to view measure as yaml:
![2018-10-24 21 42 23](https://user-images.githubusercontent.com/15277543/47476652-4d9e6700-d7d6-11e8-86f3-f35fdc44dda1.gif)

I did not add any input validation, for now only server side validation is working. I'm not quite sure if copying validation from service side is a good choice.

Author: Arthur Gavlyukovskiy <ag...@gmail.com>

Closes #446 from gavlyukovskiy/raw-measures.


Project: http://git-wip-us.apache.org/repos/asf/incubator-griffin/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-griffin/commit/bcfd0e26
Tree: http://git-wip-us.apache.org/repos/asf/incubator-griffin/tree/bcfd0e26
Diff: http://git-wip-us.apache.org/repos/asf/incubator-griffin/diff/bcfd0e26

Branch: refs/heads/master
Commit: bcfd0e2693d743cde61f50621e11cc351bbbb197
Parents: e4d587d
Author: Arthur Gavlyukovskiy <ag...@gmail.com>
Authored: Sun Oct 28 18:55:44 2018 -0700
Committer: Yuepeng <yu...@griffin-2454820.phx02.dev.ebayc3.com>
Committed: Sun Oct 28 18:55:44 2018 -0700

----------------------------------------------------------------------
 ui/angular/package.json                         |   2 +
 ui/angular/src/app/app.module.ts                |   8 +-
 .../create-measure.component.html               |  34 ++-
 .../create-measure/raw/raw.component.css        | 238 +++++++++++++++++++
 .../create-measure/raw/raw.component.html       | 102 ++++++++
 .../create-measure/raw/raw.component.spec.ts    |  43 ++++
 .../measure/create-measure/raw/raw.component.ts | 146 ++++++++++++
 .../measure-detail/measure-detail.component.css |   4 +
 .../measure-detail.component.html               |  22 +-
 .../measure-detail/measure-detail.component.ts  |  12 +-
 .../src/app/service/measure-format.service.ts   |  48 ++++
 11 files changed, 649 insertions(+), 10 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/bcfd0e26/ui/angular/package.json
----------------------------------------------------------------------
diff --git a/ui/angular/package.json b/ui/angular/package.json
index 6926014..91b5d16 100644
--- a/ui/angular/package.json
+++ b/ui/angular/package.json
@@ -30,6 +30,7 @@
     "core-js": "^2.4.1",
     "echarts": "^3.7.0",
     "font-awesome": "^4.7.0",
+    "js-yaml": "^3.11.2",
     "ng2-bootstrap": "1.6.3",
     "ng2-nouislider": "^1.7.6",
     "ngx-echarts": "2.3.1",
@@ -45,6 +46,7 @@
     "@types/jasminewd2": "~2.0.2",
     "@types/jquery": "^3.2.12",
     "@types/node": "~6.0.60",
+    "@types/js-yaml": "~3.11.2",
     "angular-tree-component": "^4.1.0",
     "codelyzer": "~3.1.1",
     "font-awesome": "^4.7.0",

http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/bcfd0e26/ui/angular/src/app/app.module.ts
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/app.module.ts b/ui/angular/src/app/app.module.ts
index 196fdbc..1598841 100644
--- a/ui/angular/src/app/app.module.ts
+++ b/ui/angular/src/app/app.module.ts
@@ -59,6 +59,7 @@ import {LoaderService} from './loader/loader.service';
 import {LoaderComponent} from './loader/loader.component';
 import {JobDetailComponent} from './job/job-detail/job-detail.component';
 import {StreamingComponent} from './job/create-job/streaming/streaming.component';
+import {RawComponent} from "./measure/create-measure/raw/raw.component";
 
 
 const appRoutes: Routes = [
@@ -113,6 +114,10 @@ const appRoutes: Routes = [
     component: PubComponent
   },
   {
+    path: 'createmeasureraw',
+    component: RawComponent
+  },
+  {
     path: 'detailed/:name',
     component: DetailMetricComponent
   },
@@ -168,7 +173,8 @@ const appRoutes: Routes = [
     ConfigurationComponent,
     LoaderComponent,
     JobDetailComponent,
-    StreamingComponent
+    StreamingComponent,
+    RawComponent
   ],
   imports: [
     BrowserModule,

http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/bcfd0e26/ui/angular/src/app/measure/create-measure/create-measure.component.html
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/measure/create-measure/create-measure.component.html b/ui/angular/src/app/measure/create-measure/create-measure.component.html
index 22ff6b6..5aaabc3 100644
--- a/ui/angular/src/app/measure/create-measure/create-measure.component.html
+++ b/ui/angular/src/app/measure/create-measure/create-measure.component.html
@@ -180,7 +180,39 @@ under the License.
             </div>
           </div>
           <div class="panel-footer stepDesc">
-            <label>Example: any data</label> ...
+            <label>Example:</label> any data ...
+          </div>
+        </section>
+      </div>
+      <div class="col-lg-6 col-md-6 col-sm-6 ruletypes">
+        <section id="panel-4" class="panel panel-yellow" (click)="click('raw')">
+          <div class="panel-heading">
+            <span style="font-size:20px">JSON/YAML measure</span>
+            <span class="pull-right" style="font-size:20px">
+                  <span class="fa fa-arrow-circle-right"></span>
+          </span>
+          </div>
+          <div class="swMain panel-body">
+            <label class="label-definition">Definition: Submit measure with JSON/YAML measure definition utilizing
+              advanced configuration capabilities that are not available through other flows</label>
+            <ul style="border-radius:0; background: none">
+              <li>
+                <div class="selected1child" style="cursor:default">
+                  <div class="stepNumber">
+                    1
+                  </div>
+                  <span class="stepDesc text-small"> Configuration </span>
+                </div>
+              </li>
+            </ul>
+            <div>
+              <ol>
+                <li>Create measure directly from JSON/YAML</li>
+              </ol>
+            </div>
+          </div>
+          <div class="panel-footer stepDesc">
+            <label>Example:</label> measure with custom configuration like custom spark sql
           </div>
         </section>
       </div>

http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/bcfd0e26/ui/angular/src/app/measure/create-measure/raw/raw.component.css
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/measure/create-measure/raw/raw.component.css b/ui/angular/src/app/measure/create-measure/raw/raw.component.css
new file mode 100644
index 0000000..cf68d86
--- /dev/null
+++ b/ui/angular/src/app/measure/create-measure/raw/raw.component.css
@@ -0,0 +1,238 @@
+/*
+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 url('../../../../../node_modules/angular2-toaster/toaster.css');
+/* @import url('../../measure.component.css'); */
+
+label {
+  font-weight: normal;
+}
+
+.container {
+  max-height: 40vh;
+  overflow-y: scroll;
+}
+
+.swMain > ul {
+  display: table;
+  list-style: none;
+  margin: 0 0 20px;
+  padding: 10px 0;
+  position: relative;
+  width: 100%;
+  background: #f7f7f8;
+  border-radius: 5px
+}
+
+.formStep {
+  background-color: #000000;
+  border-radius: 5px;
+  padding: 10px;
+  /*height:800px;*/
+}
+
+.swMain > ul li {
+  display: table-cell;
+  text-align: center;
+  width: 1%
+}
+
+.swMain > ul li > a:before {
+  border-top: 4px solid #c8c7cc;
+  /* content: ""; */
+  display: block;
+  font-size: 0;
+  height: 1px;
+  overflow: hidden;
+  position: relative;
+  top: 21px;
+  width: 100%;
+  z-index: 1
+}
+
+.swMain > ul .stepNumber {
+  background-color: #fff;
+  border: 5px solid #c8c7cc;
+  border-radius: 100%;
+  color: #546474;
+  display: inline-block;
+  font-size: 15px;
+  height: 40px;
+  line-height: 30px;
+  position: relative;
+  text-align: center;
+  width: 40px;
+  z-index: 2
+}
+
+.swMain > ul li > a.selected .stepNumber {
+  border-color: #007AFF
+}
+
+.swMain ul li > a.done .stepNumber {
+  border-color: #007AFF;
+  background-color: #007AFF;
+  color: #fff;
+  text-indent: -9999px
+}
+
+.swMain ul li > a.done .stepNumber:before {
+  content: "\f00c";
+  display: inline;
+  float: right;
+  font-family: FontAwesome;
+  font-weight: 300;
+  height: auto;
+  text-shadow: none;
+  margin-right: 7px;
+  text-indent: 0
+}
+
+.swMain ul li > a.done.wait .stepNumber {
+  background-color: #F6F6F6 !important;
+  color: #CCC !important;
+  text-indent: 0 !important
+}
+
+.swMain ul li > a.done.wait .stepNumber:before {
+  content: "" !important
+}
+
+.swMain > ul li .stepDesc {
+  color: #8e8e93;
+  display: block;
+  font-size: 14px;
+  margin-top: 4px;
+  max-width: 100%;
+  table-layout: fixed;
+  text-align: center;
+  word-wrap: break-word;
+  z-index: 104
+}
+
+.swMain li > a.done .stepDesc,
+.swMain > ul li > a.selected .stepDesc {
+  /*color: #2B3D53*/
+  color: #007AFF
+}
+
+.swMain > ul li > a.disabled {
+  cursor: default
+}
+
+.swMain .progress {
+  margin-bottom: 30px
+}
+
+.swMain .stepContainer {
+  height: auto !important
+}
+
+.swMain .y-scrollable {
+  overflow-y: auto;
+  overflow-x: hidden;
+  max-height: 600px;
+}
+
+fieldset {
+  border: 1px solid #e6e8e8;
+  border-radius: 5px;
+  margin: 20px 0;
+  padding: 25px;
+  position: relative;
+  min-width: 0;
+  display: block;
+}
+
+fieldset legend {
+  background-color: #000000;
+  left: 10px;
+  padding: 0 10px;
+  position: absolute;
+  top: -12px;
+  color: #fff;
+  font-weight: 400;
+  width: auto !important;
+  border: none !important;
+}
+
+.btn-o {
+  background: 0 0 !important;
+}
+
+.btn-wide {
+  min-width: 120px;
+}
+
+.btn-container {
+  height: 50px;
+  /*position: absolute;
+  bottom: 10;*/
+}
+
+.btn-flat {
+  outline: none !important;
+}
+
+::-webkit-scrollbar {
+  width: 6px;
+}
+
+::-webkit-scrollbar-track {
+  background-color: #eaeaea;
+  /*background-color: #0a0a0a;*/
+  border-left: 1px solid #ccc;
+}
+
+::-webkit-scrollbar-thumb {
+  background-color: #ccc;
+}
+
+::-webkit-scrollbar-thumb:hover {
+  background-color: #aaa;
+}
+
+.disabled {
+  pointer-events: none;
+  cursor: default;
+  opacity: 0.6;
+}
+
+a {
+  color: white;
+}
+
+.code-viewport {
+  background-color: #333333;
+  border: 1px solid #1d1d1d;
+  border-radius: 4px;
+  font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
+  font-size: 13px;
+  line-height: 1.5;
+  color: #e4e4e4;
+  word-break: break-all;
+  word-wrap: break-word;
+  white-space: pre;
+  overflow: auto;
+  resize: none;
+  outline: none;
+}
+
+.fixed-textarea {
+  height: 450px;
+}

http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/bcfd0e26/ui/angular/src/app/measure/create-measure/raw/raw.component.html
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/measure/create-measure/raw/raw.component.html b/ui/angular/src/app/measure/create-measure/raw/raw.component.html
new file mode 100644
index 0000000..ff2f4da
--- /dev/null
+++ b/ui/angular/src/app/measure/create-measure/raw/raw.component.html
@@ -0,0 +1,102 @@
+<!--
+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.
+-->
+<div class="container-fluid" (window:resize)="onResize($event)">
+  <div class="row">
+    <h5 class="over-title margin-bottom-15">Create Measure</h5>
+  </div>
+  <div class="row">
+    <form name="Form" id="form" #prForm="ngForm" novalidate>
+      <div id="wizard" class="swMain">
+        <ul>
+          <li>
+            <a class="selected">
+              <div class="stepNumber">
+                1
+              </div>
+              <span class="stepDesc text-small"> Configuration </span>
+            </a>
+          </li>
+        </ul>
+      </div>
+      <div id="step-1" class="formStep">
+        <label class="stepDesc">Please setup the measure required information</label>
+        <div class="container-fluid">
+          <div class="col-md-12 col-lg-12 col-sm-12">
+            <fieldset>
+              <legend>
+                Required Information
+              </legend>
+              <div class="y-scrollable">
+                <div class="col-md-12 col-lg-12 col-sm-12" style="margin-top:30px;">
+                  <div class="form-group"
+                       [ngClass]="{'has-error': !valid, 'has-success': valid}">
+                    <div class="row">
+                      <label class="col-md-2 col-lg-2 col-sm-2 control-label">
+                        Raw measure<span class="symbol required"></span>:
+                      </label>
+                      <div class="col-md-10 col-lg-10 col-sm-10">
+                        <div class="btn-group btn-group-sm navbar-right" role="group" aria-label="Format">
+                          <button type="button" class="btn btn-flat btn-primary" [ngClass]="{'active': format == Format.json}" (click)="changeFormat(Format.json)">JSON</button>
+                          <button type="button" class="btn btn-flat btn-primary" [ngClass]="{'active': format == Format.yaml}" (click)="changeFormat(Format.yaml)">YAML</button>
+                        </div>
+                      </div>
+                    </div>
+                    <div class="row">
+                      <textarea class="code-viewport fixed-textarea" required [(ngModel)]="data" #content="ngModel" name="content" (ngModelChange)="onInputChange()"></textarea>
+                      <span class="error text-small block " *ngIf="!valid">Measure is not valid</span>
+                    </div>
+                  </div>
+                </div>
+                <div style="color:#b2c831">
+                  <p>
+                    <i class="fa fa-info-circle"></i> After submitted, please go to "<a class="bark-link"
+                                                                                        routerLink="/measures">Measures</a>"
+                    to check the measure status
+                  </p>
+                </div>
+              </div>
+            </fieldset>
+          </div>
+          <div class="form-group btn-container">
+            <toaster-container></toaster-container>
+            <button type="submit" (click)="submit(prForm)" class="btn btn-primary btn-o next-step btn-wide pull-right">
+              Submit
+            </button>
+          </div>
+        </div>
+      </div>
+      <div class="modal fade" id="confirm" role="dialog" #modal tabindex="-1" [ngClass]="{'in': visibleAnimate}"
+           [ngStyle]="{'display': visible ? 'block' : 'none', 'opacity': visibleAnimate ? 1 : 0}"
+           (click)="onContainerClicked($event)">
+        <div class="modal-dialog modal-xg modal-lg">
+          <div class="modal-content">
+            <div class="modal-header">
+              <button type="button" class="close" (click)="hide()">&times;</button>
+              <h4 class="modal-title">Save the measure?</h4>
+            </div>
+            <div class="modal-footer">
+              <button type="button" class="btn btn-default" (click)="hide()">Cancel</button>
+              <button type="button" id="save" class="btn btn-primary" (click)="save()">Save</button>
+            </div>
+          </div>
+        </div>
+      </div>
+    </form>
+  </div>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/bcfd0e26/ui/angular/src/app/measure/create-measure/raw/raw.component.spec.ts
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/measure/create-measure/raw/raw.component.spec.ts b/ui/angular/src/app/measure/create-measure/raw/raw.component.spec.ts
new file mode 100644
index 0000000..15a7a97
--- /dev/null
+++ b/ui/angular/src/app/measure/create-measure/raw/raw.component.spec.ts
@@ -0,0 +1,43 @@
+/*
+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 {async, ComponentFixture, TestBed} from '@angular/core/testing';
+
+import {RawComponent} from './raw.component';
+
+describe('RawComponent', () => {
+  let component: RawComponent;
+  let fixture: ComponentFixture<RawComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [RawComponent]
+    })
+      .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(RawComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should be created', () => {
+    expect(component).toBeTruthy();
+  });
+});

http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/bcfd0e26/ui/angular/src/app/measure/create-measure/raw/raw.component.ts
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/measure/create-measure/raw/raw.component.ts b/ui/angular/src/app/measure/create-measure/raw/raw.component.ts
new file mode 100644
index 0000000..743cd59
--- /dev/null
+++ b/ui/angular/src/app/measure/create-measure/raw/raw.component.ts
@@ -0,0 +1,146 @@
+/*
+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 {Component} from "@angular/core";
+import {ServiceService} from "../../../service/service.service";
+import {TREE_ACTIONS, ITreeOptions} from "angular-tree-component";
+import {ToasterService} from "angular2-toaster";
+import * as $ from "jquery";
+import {HttpClient} from "@angular/common/http";
+import {ActivatedRoute, Router} from "@angular/router";
+import {AfterViewChecked, ElementRef} from "@angular/core";
+import {MeasureFormatService, Format} from "../../../service/measure-format.service";
+
+@Component({
+  selector: "app-raw",
+  templateUrl: "./raw.component.html",
+  providers: [ServiceService, MeasureFormatService],
+  styleUrls: ["./raw.component.css"]
+})
+export class RawComponent implements AfterViewChecked {
+
+  constructor(
+    private elementRef: ElementRef,
+    private toasterService: ToasterService,
+    private measureFormatService: MeasureFormatService,
+    private http: HttpClient,
+    private router: Router,
+    public serviceService: ServiceService
+  ) {
+  }
+
+  data = "";
+  valid = false;
+  Format: typeof Format = Format;
+  format: Format;
+  createResult: any;
+  public visible = false;
+  public visibleAnimate = false;
+
+  public hide(): void {
+    this.visibleAnimate = false;
+    setTimeout(() => (this.visible = false), 300);
+    $("#save").removeAttr("disabled");
+  }
+
+  public onContainerClicked(event: MouseEvent): void {
+    if ((<HTMLElement>event.target).classList.contains("modal")) {
+      this.hide();
+    }
+  }
+
+  onResize(event) {
+    this.resizeWindow();
+  }
+
+  ngAfterViewChecked() {
+    this.resizeWindow();
+  }
+
+  resizeWindow() {
+    var stepSelection = ".formStep";
+    $(stepSelection).css({
+      height: window.innerHeight - $(stepSelection).offset().top
+    });
+    $("fieldset").height(
+      $(stepSelection).height() -
+      $(stepSelection + ">.stepDesc").height() -
+      $(".btn-container").height() -
+      130
+    );
+    $(".y-scrollable").css({
+      height: $("fieldset").height()
+    });
+  }
+
+  submit(form) {
+    if (!form.valid) {
+      this.toasterService.pop(
+        "error",
+        "Error!",
+        "please complete the form in this step before proceeding"
+      );
+      return false;
+    }
+    this.visible = true;
+    setTimeout(() => (this.visibleAnimate = true), 100);
+  }
+
+  save() {
+    let measure2Save = this.measureFormatService.parse(this.data, this.format);
+    console.log(measure2Save);
+    let addModels = this.serviceService.config.uri.addModels;
+    $("#save").attr("disabled", "true");
+    this.http.post(addModels, measure2Save).subscribe(
+      data => {
+        this.createResult = data;
+        this.hide();
+        this.router.navigate(["/measures"]);
+      },
+      err => {
+        let response = JSON.parse(err.error);
+        if (response.code === '40901') {
+          this.toasterService.pop("error", "Error!", "Measure name already exists!");
+        } else {
+          this.toasterService.pop("error", "Error!", response.message);
+        }
+        console.log("Error when creating measure");
+      }
+    );
+  }
+
+  onInputChange() {
+    let format = this.measureFormatService.determineFormat(this.data);
+    if (format) {
+      this.format = format;
+      this.valid = true;
+    }
+    else {
+      this.format = null;
+      this.valid = false;
+    }
+  }
+
+  changeFormat(format: Format) {
+    if (this.valid) {
+      let content = this.measureFormatService.parse(this.data, this.format);
+      this.data = this.measureFormatService.format(content, format);
+      this.format = format;
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/bcfd0e26/ui/angular/src/app/measure/measure-detail/measure-detail.component.css
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/measure/measure-detail/measure-detail.component.css b/ui/angular/src/app/measure/measure-detail/measure-detail.component.css
index 9e8c026..034d248 100644
--- a/ui/angular/src/app/measure/measure-detail/measure-detail.component.css
+++ b/ui/angular/src/app/measure/measure-detail/measure-detail.component.css
@@ -28,3 +28,7 @@ under the License.
   word-break: break-all;
   word-wrap: break-word;
 }
+
+.btn-flat {
+  outline: none !important;
+}

http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/bcfd0e26/ui/angular/src/app/measure/measure-detail/measure-detail.component.html
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/measure/measure-detail/measure-detail.component.html b/ui/angular/src/app/measure/measure-detail/measure-detail.component.html
index 02871de..be02599 100644
--- a/ui/angular/src/app/measure/measure-detail/measure-detail.component.html
+++ b/ui/angular/src/app/measure/measure-detail/measure-detail.component.html
@@ -190,11 +190,25 @@ under the License.
         {{index.name}}&nbsp;:&nbsp;{{index.infos}}
       </div>
     </div>
-    <h5 class="row">Rules JSON</h5>
+    <h5 class="row">Rules</h5>
     <div *ngIf="ruleData['evaluate.rule']">
-      <a *ngIf="!showFullRules" (click)="showFullRules=true">Show full JSON</a>
-      <a *ngIf="showFullRules" (click)="showFullRules=false">Show rules only</a>
-      <pre class="code-viewport">{{ getJsonContent() }}</pre>
+      <div class="row">
+        <div class="col-md-6 col-lg-6 col-sm-6">
+          <label class="form-check-label">
+            Show rules only:
+            <input class="form-check-input" type="checkbox" [checked]="!showFullRules" (change)="showFullRules = !showFullRules">
+          </label>
+        </div>
+        <div class="col-md-6 col-lg-6 col-sm-6">
+          <div class="btn-group btn-group-sm navbar-right" role="group" aria-label="Format">
+            <button type="button" class="btn btn-flat btn-primary" [ngClass]="{'active': format == Format.json}" (click)="format=Format.json">JSON</button>
+            <button type="button" class="btn btn-flat btn-primary" [ngClass]="{'active': format == Format.yaml}" (click)="format=Format.yaml">YAML</button>
+          </div>
+        </div>
+      </div>
+      <div class="row">
+        <pre class="code-viewport">{{ getRawContent() }}</pre>
+      </div>
     </div>
   </div>
 </div>

http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/bcfd0e26/ui/angular/src/app/measure/measure-detail/measure-detail.component.ts
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/measure/measure-detail/measure-detail.component.ts b/ui/angular/src/app/measure/measure-detail/measure-detail.component.ts
index a30f4fb..5ad5fae 100644
--- a/ui/angular/src/app/measure/measure-detail/measure-detail.component.ts
+++ b/ui/angular/src/app/measure/measure-detail/measure-detail.component.ts
@@ -17,15 +17,16 @@ specific language governing permissions and limitations
 under the License.
 */
 import {Component, OnInit} from "@angular/core";
-import {Router, ActivatedRoute, ParamMap} from "@angular/router";
+import {ActivatedRoute, Router} from "@angular/router";
 import "rxjs/add/operator/switchMap";
 import {HttpClient} from "@angular/common/http";
 import {ServiceService} from "../../service/service.service";
+import {MeasureFormatService, Format} from "../../service/measure-format.service";
 
 @Component({
   selector: "app-measure-detail",
   templateUrl: "./measure-detail.component.html",
-  providers: [ServiceService],
+  providers: [ServiceService, MeasureFormatService],
   styleUrls: ["./measure-detail.component.css"]
 })
 export class MeasureDetailComponent implements OnInit {
@@ -35,6 +36,7 @@ export class MeasureDetailComponent implements OnInit {
     private route: ActivatedRoute,
     private router: Router,
     private http: HttpClient,
+    private measureFormatService: MeasureFormatService,
     public serviceService: ServiceService
   ) {
   }
@@ -42,6 +44,8 @@ export class MeasureDetailComponent implements OnInit {
   ruleData: any;
   getModelUrl: string;
   showFullRules: boolean;
+  Format: typeof Format = Format;
+  format: Format = Format.json;
   ruleDes = [];
   sourceLength: number;
   sourceDB: string;
@@ -112,13 +116,13 @@ export class MeasureDetailComponent implements OnInit {
     );
   }
 
-  getJsonContent() {
+  getRawContent() {
     let content;
     if (!this.showFullRules) {
       content = (this.ruleData['evaluate.rule'] || {})['rules'];
     } else {
       content = this.ruleData;
     }
-    return JSON.stringify(content, null, 4);
+    return this.measureFormatService.format(content, this.format);
   }
 }

http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/bcfd0e26/ui/angular/src/app/service/measure-format.service.ts
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/service/measure-format.service.ts b/ui/angular/src/app/service/measure-format.service.ts
new file mode 100644
index 0000000..a840470
--- /dev/null
+++ b/ui/angular/src/app/service/measure-format.service.ts
@@ -0,0 +1,48 @@
+import {Injectable} from "@angular/core";
+import * as yaml from "js-yaml";
+
+@Injectable()
+export class MeasureFormatService {
+  constructor() {
+  }
+
+  format(measure: any, format: Format) {
+    switch (format) {
+      case Format.json:
+        return JSON.stringify(measure, null, 4);
+      case Format.yaml:
+        return yaml.dump(measure);
+
+    }
+  }
+
+  parse(data: string, format: Format) {
+    switch (format) {
+      case Format.json:
+        return JSON.parse(data);
+      case Format.yaml:
+        return yaml.load(data);
+
+    }
+  }
+
+  determineFormat(data: string) {
+    try {
+      JSON.parse(data);
+      return Format.json;
+    } catch (e) {}
+    try {
+      if (yaml.load(data)) {
+        return Format.yaml;
+      }
+    } catch (e) {}
+    return null;
+  }
+}
+
+export enum Format {
+  json = 1,
+  yaml = 2
+}
+
+