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 2017/10/11 04:31:11 UTC
[11/13] incubator-griffin git commit: refactor ui with angular2
http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/42ee8863/ui/angular/src/app/measure/create-measure/ac/ac.component.ts
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/measure/create-measure/ac/ac.component.ts b/ui/angular/src/app/measure/create-measure/ac/ac.component.ts
new file mode 100644
index 0000000..122d7b7
--- /dev/null
+++ b/ui/angular/src/app/measure/create-measure/ac/ac.component.ts
@@ -0,0 +1,594 @@
+/*
+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, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
+import { FormsModule, Validator} from '@angular/forms';
+import {ServiceService} from '../../../service/service.service';
+// import { PatternValidator } from '@angular/forms';
+
+
+import { TREE_ACTIONS, KEYS, IActionMapping, ITreeOptions } from 'angular-tree-component';
+import { BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import { ToasterModule, ToasterService} from 'angular2-toaster';
+import * as $ from 'jquery';
+import { HttpClient} from '@angular/common/http';
+import { Router} from "@angular/router";
+
+
+class node {
+ name: string;
+ id: number;
+ children:object[];
+ isExpanded:boolean;
+ cols:Col[];
+ parent:string;
+};
+class Col{
+ name:string;
+ type:string;
+ comment:string;
+ selected :boolean;
+ constructor(name:string,type:string,comment:string,selected:boolean){
+ this.name = name;
+ this.type = type;
+ this.comment = comment;
+ this.selected = false;
+ }
+ getSelected(){
+ return this.selected;
+ }
+ setSelected(selected){
+ this.selected = selected;
+ }
+}
+
+@Component({
+ selector: 'app-ac',
+ templateUrl: './ac.component.html',
+ providers:[ServiceService],
+ styleUrls: ['./ac.component.css']
+})
+
+export class AcComponent implements OnInit {
+
+ currentStep = 1;
+ org:string;
+ desc:string;
+ selection = [];
+ selectedAll = false;
+ selectedAllTarget = false;
+ selectionTarget = [];
+ map = [];
+ mappings = [];
+ matches = [];
+ dataAsset = '';
+ rules = '';
+ currentDB = '';
+ currentTable = '';
+ currentDBTarget = '';
+ currentTableTarget = '';
+ schemaCollection:Col[];
+ schemaCollectionTarget:Col[];
+ matchFunctions = ['=', '!=', '>', '>=','<',"<="];
+ data:any;
+
+ measureTypes = ['accuracy','validity','anomaly detection','publish metrics'];
+ type = 'accuracy';
+ newMeasure = {
+ "name":'',
+ "process.type": "batch",
+ "owner":"",
+ "description":"",
+ "organization":"",
+ "data.sources": [
+ {
+ "name": "source",
+ "connectors": [
+ {
+ "type": "HIVE",
+ "version": "1.2",
+ "config":{
+ "database":'',
+ "table.name":'',
+ }
+ }
+ ]
+ }, {
+ "name": "target",
+ "connectors": [
+ {
+ "type": "HIVE",
+ "version": "1.2",
+ "config":{
+ "database":'',
+ "table.name":'',
+ }
+ }
+ ]
+ }
+ ],
+
+ "evaluateRule":{
+ "rules": [
+ {
+ "dsl.type": "griffin-dsl",
+ "dq.type": "accuracy",
+ "rule": ""
+ // "details": {
+ // "source": "source",
+ // "target": "target",
+ // "miss.records": {
+ // "name": "miss.records",
+ // "persist.type": "record"
+ // },
+ // "accuracy": {
+ // "name": "accu",
+ // "persist.type": "metric"
+ // },
+ // "miss": "miss",
+ // "total": "total",
+ // "matched": "matched"
+ // }
+ }
+ ]
+ }
+ };
+ name:'';
+ evaluateRule:any;
+ // desc:'';
+ // org:'';
+ owner = 'test';
+ createResult :any;
+
+ private toasterService: ToasterService;
+ public visible = false;
+ public visibleAnimate = false;
+
+ public hide(): void {
+ this.visibleAnimate = false;
+ setTimeout(() => this.visible = false, 300);
+ }
+
+ public onContainerClicked(event: MouseEvent): void {
+ if ((<HTMLElement>event.target).classList.contains('modal')) {
+ this.hide();
+ }
+ }
+
+ addMapping(x,i){
+ this.mappings[i] = x;
+ }
+
+ toggleSelection (row) {
+ row.selected = !row.selected;
+ var idx = this.selection.indexOf(row.name);
+ // is currently selected
+ if (idx > -1) {
+ this.selection.splice(idx, 1);
+ this.selectedAll = false;
+ }
+ // is newly selected
+ else {
+ this.selection.push(row.name);
+ }
+ };
+
+ toggleSelectionTarget (row) {
+ row.selected = !row.selected;
+ var idx = this.selectionTarget.indexOf(row.name);
+ // is currently selected
+ if (idx > -1) {
+ this.selectionTarget.splice(idx, 1);
+ this.selectedAllTarget = false;
+ }
+ // is newly selected
+ else {
+ this.selectionTarget.push(row.name);
+ }
+ let l = this.selectionTarget.length;
+ for(let i =0;i<l;i++)
+ this.matches[i] = "=";
+ };
+
+ toggleAll () {
+ this.selectedAll = !this.selectedAll;
+ this.selection = [];
+ for(var i =0; i < this.schemaCollection.length; i ++){
+ this.schemaCollection[i].selected = this.selectedAll;
+ if (this.selectedAll) {
+ this.selection.push(this.schemaCollection[i].name);
+ this.matches[i] = "=";
+ }
+ }
+ };
+
+ toggleAllTarget () {
+ this.selectedAllTarget = !this.selectedAllTarget;
+ this.selectionTarget = [];
+ for(var i =0; i < this.schemaCollectionTarget.length; i ++){
+ this.schemaCollectionTarget[i].selected = this.selectedAllTarget;
+ if (this.selectedAllTarget) {
+ this.selectionTarget.push(this.schemaCollectionTarget[i].name);
+ }
+ }
+ };
+
+ next (form) {
+ if(this.formValidation(this.currentStep)){
+ this.currentStep++;
+ }else{
+ this.toasterService.pop('error','Error!','Please select at least one attribute!');
+ return false;
+ }
+ }
+
+ formValidation = function(step) {
+ if (step == undefined) {
+ step = this.currentStep;
+ }
+ if (step == 1) {
+ return this.selection && this.selection.length > 0;
+ } else if (step == 2) {
+ return (this.selectionTarget && this.selectionTarget.length > 0)//at least one target is selected
+ // && !((this.currentTable.name == this.currentTableTarget.name)&&(this.currentDB.name == this.currentDBTarget.name));//target and source should be different
+ } else if (step == 3) {
+ return this.selectionTarget && this.selectionTarget.length == this.mappings.length
+ && this.mappings.indexOf('') == -1
+ } else if (step == 4) {
+ }
+ return false;
+ }
+
+ prev (form) {
+ this.currentStep--;
+ }
+ goTo (i) {
+ this.currentStep = i;
+ }
+ submit (form) {
+ // form.$setPristine();
+ if (!form.valid) {
+ this.toasterService.pop('error', 'Error!', 'please complete the form in this step before proceeding');
+ return false;
+ }
+ var rule = '';
+ this.newMeasure = {
+ "name":this.name,
+ "process.type": "batch",
+ "owner":this.owner,
+ "description":this.desc,
+ "organization":this.org,
+ "data.sources": [
+ {
+ "name": "source",
+ "connectors": [
+ {
+ "type": "HIVE",
+ "version": "1.2",
+ "config":{
+ "database":this.currentDB,
+ "table.name":this.currentTable,
+ }
+ }
+ ]
+ }, {
+ "name": "target",
+ "connectors": [
+ {
+ "type": "HIVE",
+ "version": "1.2",
+ "config":{
+ "database":this.currentDBTarget,
+ "table.name":this.currentTableTarget,
+ }
+ }
+ ]
+ }
+ ],
+
+ "evaluateRule":{
+ "rules": [
+ {
+ "dsl.type": "griffin-dsl",
+ "dq.type": "accuracy",
+ "rule": ""
+ // "details": {
+ // "source": "source",
+ // "target": "target",
+ // "miss.records": {
+ // "name": "miss.records",
+ // "persist.type": "record"
+ // },
+ // "accuracy": {
+ // "name": "accu",
+ // "persist.type": "metric"
+ // },
+ // "miss": "miss",
+ // "total": "total",
+ // "matched": "matched"
+ // }
+ }
+ ]
+ }
+ };
+ var mappingRule = function(src, tgt, matches) {
+ var rules;
+ rules = 'source.' + src + matches + 'target.' + tgt
+ return rules;
+ }
+ var self = this;
+ var rules = this.selectionTarget.map(function(item, i) {
+ return mappingRule(self.selection[i], item, self.matches[i]);
+ });
+ rule = rules.join(" AND ");
+ this.rules = rule;
+ this.newMeasure.evaluateRule.rules[0].rule = rule;
+ // for(var i =0; i < this.selectionTarget.length; i ++){
+ // this.newMeasure.mappings.push({target:this.selectionTarget[i],
+ // // src:this.mappings[i],
+ // matchMethod: this.matches[i]});
+ // }
+ this.visible = true;
+ setTimeout(() => this.visibleAnimate = true, 100);
+ }
+
+ save() {
+
+ var addModels = this.servicecService.config.uri.addModels;
+
+ this.http
+ .post(addModels, this.newMeasure)
+ .subscribe(data => {
+ this.createResult = data;
+ this.hide();
+ this.router.navigate(['/measures']);
+ // var self = this;
+ // setTimeout(function () {
+ // self.hide();
+ // self.router.navigate(['/measures']);
+ // },0)
+ },
+ err => {
+ console.log('Something went wrong!');
+ });
+
+ }
+
+ // data: { [key: string]: Array<object>; } = {
+ // "default": [
+ // {
+ // "tableName": "ext",
+ // "dbName": "default",
+ // "owner": "hadoop",
+ // "createTime": 1488353464,
+ // "lastAccessTime": 0,
+ // "retention": 0,
+ // "sd": {
+ // "cols": [
+ // {
+ // "name": "id",
+ // "type": "int",
+ // "comment": null,
+ // "setName": true,
+ // "setComment": false,
+ // "setType": true
+ // },
+ // {
+ // "name": "name",
+ // "type": "string",
+ // "comment": null,
+ // "setName": true,
+ // "setComment": false,
+ // "setType": true
+ // },
+ // {
+ // "name": "age",
+ // "type": "int",
+ // "comment": null,
+ // "setName": true,
+ // "setComment": false,
+ // "setType": true
+ // }
+ // ],
+ // "location": "hdfs://10.9.246.187/user/hive/ext",
+ // },
+ // },
+ // {
+ // "tableName": "ext1",
+ // "dbName": "default",
+ // "owner": "hadoop",
+ // "createTime": 1489382943,
+ // "lastAccessTime": 0,
+ // "retention": 0,
+ // "sd": {
+ // "cols": [
+ // {
+ // "name": "id",
+ // "type": "int",
+ // "comment": null,
+ // "setName": true,
+ // "setComment": false,
+ // "setType": true
+ // },
+ // {
+ // "name": "name",
+ // "type": "string",
+ // "comment": null,
+ // "setName": true,
+ // "setComment": false,
+ // "setType": true
+ // },
+ // {
+ // "name": "age",
+ // "type": "int",
+ // "comment": null,
+ // "setName": true,
+ // "setComment": false,
+ // "setType": true
+ // }
+ // ],
+ // "location": "hdfs://10.9.246.187/user/hive/ext1",
+ // },
+ // }
+ // ],
+ // "griffin": [
+ // {
+ // "tableName": "avr_out",
+ // "dbName": "griffin",
+ // "owner": "hadoop",
+ // "createTime": 1493892603,
+ // "lastAccessTime": 0,
+ // "retention": 0,
+ // "sd": {
+ // "cols": [
+ // {
+ // "name": "id",
+ // "type": "bigint",
+ // "comment": null,
+ // "setName": true,
+ // "setComment": false,
+ // "setType": true
+ // },
+ // {
+ // "name": "age",
+ // "type": "int",
+ // "comment": null,
+ // "setName": true,
+ // "setComment": false,
+ // "setType": true
+ // },
+ // {
+ // "name": "desc",
+ // "type": "string",
+ // "comment": null,
+ // "setName": true,
+ // "setComment": false,
+ // "setType": true
+ // }
+ // ],
+ // "location": "hdfs://10.9.246.187/griffin/data/batch/avr_out",
+ // },
+ // }
+ // ],
+ // };
+
+ options: ITreeOptions = {
+ displayField: 'name',
+ isExpandedField: 'expanded',
+ idField: 'id',
+ actionMapping: {
+ mouse: {
+ click: (tree, node, $event) => {
+ if (node.hasChildren) {
+ this.currentDB = node.data.name;
+ this.currentTable = '';
+ TREE_ACTIONS.TOGGLE_EXPANDED(tree, node, $event);
+ }
+ else if(node.data.cols)
+ {
+ this.currentTable = node.data.name;
+ this.currentDB = node.data.parent;
+ this.schemaCollection = node.data.cols;
+ }
+ }
+ }
+ },
+ animateExpand: true,
+ animateSpeed: 30,
+ animateAcceleration: 1.2
+ };
+
+ targetOptions: ITreeOptions = {
+ displayField: 'name',
+ isExpandedField: 'expanded',
+ idField: 'id',
+ actionMapping: {
+ mouse: {
+ click: (tree, node, $event) => {
+ if (node.hasChildren) {
+ this.currentDBTarget = node.data.name;
+ this.currentTableTarget = '';
+ TREE_ACTIONS.TOGGLE_EXPANDED(tree, node, $event);
+ }
+ else if(node.data.cols)
+ {
+ this.currentTableTarget = node.data.name;
+ this.currentDBTarget = node.data.parent;
+ this.schemaCollectionTarget = node.data.cols;
+ }
+ }
+ }
+ },
+ animateExpand: true,
+ animateSpeed: 30,
+ animateAcceleration: 1.2
+ };
+
+ nodeList:object[];
+ nodeListTarget:object[];
+ constructor(toasterService: ToasterService,private http: HttpClient,private router:Router,public servicecService:ServiceService) {
+ this.toasterService = toasterService;
+ };
+
+ onResize(event){
+ console.log("Width: " + event.target.innerWidth);
+ this.resizeWindow();
+ }
+
+ resizeWindow(){
+ var stepSelection = '.formStep[id=step-' + this.currentStep + ']';
+ $(stepSelection).css({
+ height: window.innerHeight - $(stepSelection).offset().top
+ });
+ }
+
+ ngOnInit() {
+ var allDataassets = this.servicecService.config.uri.dataassetlist;
+ this.http.get(allDataassets).subscribe(data =>{
+ this.nodeList = new Array();
+ let i = 1;
+ this.data = data;
+ for (let db in this.data) {
+ let new_node = new node();
+ new_node.name = db;
+ new_node.id = i;
+ new_node.isExpanded = true;
+ i++;
+ new_node.children = new Array();
+ for(let i = 0;i<this.data[db].length;i++){
+ let new_child = new node();
+ new_child.name = this.data[db][i]['tableName'];
+ new_node.children.push(new_child);
+ new_child.isExpanded = false;
+ new_child.parent = db;
+ new_child.cols = Array<Col>();
+ for(let j = 0;j<this.data[db][i]['sd']['cols'].length;j++){
+ let new_col = new Col(this.data[db][i]['sd']['cols'][j].name,
+ this.data[db][i]['sd']['cols'][j].type,
+ this.data[db][i]['sd']['cols'][j].comment,false);
+ new_child.cols.push(new_col);
+ }
+ }
+ this.nodeList.push(new_node);
+ }
+ this.nodeListTarget = JSON.parse(JSON.stringify(this.nodeList));
+ });
+
+ };
+}
http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/42ee8863/ui/angular/src/app/measure/create-measure/create-measure.component.css
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/measure/create-measure/create-measure.component.css b/ui/angular/src/app/measure/create-measure/create-measure.component.css
new file mode 100644
index 0000000..4e3c6f7
--- /dev/null
+++ b/ui/angular/src/app/measure/create-measure/create-measure.component.css
@@ -0,0 +1,123 @@
+/*
+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');
+
+div.tree div.tree-children::before,
+div.tree::before {
+ content: "";
+ position: absolute;
+ border-left: 1px dotted #23527c;
+ height: 100%;
+ top: -14px;
+ left: 12px
+}
+
+div.tree {
+ padding-left: 0;
+ margin-left: -5px
+}
+
+div.tree div.tree-children {
+ position: relative;
+ padding-left: 0;
+ margin-left: 16px
+}
+
+div.tree div.tree-children::before {
+ left: 5px
+}
+
+div.tree treenode>div>.node-wrapper {
+ margin-left: 24px
+}
+
+div.tree treenode>div>.node-wrapper>.node-content-wrapper {
+ margin-left: 4px
+}
+
+div.tree treenode>div.tree-node-leaf>.node-wrapper {
+ margin-left: 0
+}
+
+div.tree treenode>div::before {
+ content: "";
+ position: absolute;
+ border-bottom: 1px dotted #23527c;
+ width: 7px;
+ margin-top: 12px;
+ left: 7px
+}
+
+div.tree treenode>div .toggle-children-wrapper {
+ width: 13px;
+ height: 13px;
+ border: 1px solid #23527c;
+ position: absolute;
+ left: 15px;
+ margin-top: 5px;
+ margin-left: 0;
+ display: inline-block;
+ background-color: #fff;
+ z-index: 1
+}
+
+div.tree treenode>div .toggle-children-wrapper::before {
+ content: "";
+ display: inline-block;
+ width: 7px;
+ border-top: 1px solid #23527c;
+ position: absolute;
+ top: 5px;
+ left: 2px
+}
+
+div.tree treenode>div .toggle-children-wrapper.toggle-children-wrapper-collapsed::after {
+ content: "";
+ display: inline-block;
+ height: 7px;
+ border-left: 1px solid #23527c;
+ position: absolute;
+ top: 2px;
+ left: 5px
+}
+
+div.tree treenode>div .toggle-children-wrapper .toggle-children {
+ display: none
+}
+
+div.tree treenode>div .node-content-wrapper {
+ margin-left: 4px
+}
+
+div.tree>treenode>div::before {
+ left: 14px
+}
+
+div.tree>treenode>div>.node-wrapper>treenodeexpander>.toggle-children-wrapper {
+ left: 22px
+}
+
+.panel{
+ background-color: #222222;
+}
+
+.panel-footer{
+ background-color: #3c3c3c;
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/42ee8863/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
new file mode 100644
index 0000000..fd8bdcb
--- /dev/null
+++ b/ui/angular/src/app/measure/create-measure/create-measure.component.html
@@ -0,0 +1,134 @@
+<!--
+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" id="main" >
+ <div class="row">
+ <h5 class="over-title margin-bottom-15">Create DQ Model</h5>
+ </div>
+ <div class="row">
+ <div class="col-lg-6 col-md-6 col-sm-6 ruletypes">
+ <section id="panel-1" class="panel panel-red" style="cursor:pointer" (click)="click('ac')">
+ <div class="panel-heading">
+ <span style="font-size:20px">Accuracy</span>
+ <span class="pull-right" style="font-size:20px">
+ <span class="fa fa-arrow-circle-right" (click)="click('ac')"></span>
+ </span>
+ </div>
+ <div class="swMain panel-body" >
+ <label class="label-definition">Definition: Measured by how the values agree with an identified source of truth</label>
+ <ul style="border-radius:0; background: none">
+ <li >
+ <a class="selected" >
+ <div class="stepNumber">
+ 1
+ </div>
+ <span class="stepDesc text-small"> Choose Source </span>
+ </a>
+ </li>
+ <li>
+ <a>
+ <div class="stepNumber">
+ 2
+ </div>
+ <span class="stepDesc text-small"> Choose Target </span>
+ </a>
+ </li>
+ <li >
+ <a>
+ <div class="stepNumber">
+ 3
+ </div>
+ <span class="stepDesc text-small"> Mapping Source and Target </span>
+ </a>
+ </li>
+ <li>
+ <a>
+ <div class="stepNumber">
+ 4
+ </div>
+ <span class="stepDesc text-small"> Configuration </span>
+ </a>
+ </li>
+ </ul>
+ <div>
+ <ol>
+ <li>Select the source dataset and fields which will be used for comparision</li>
+ <li>Select the target dataset and fields which will be used for comparision</li>
+ <li>Mapping the target fields with source</li>
+ <li>Set basic configuration for your model (name, system, threshold, etc.)</li>
+ </ol>
+ </div>
+ </div>
+ <div class="panel-footer stepDesc">
+ <label>Example:</label> suppose source table A has 1000 records and target table B only has 999 records pefectly matched with A for selected fields, then
+ Accuracy Rate(%) = 999/1000 * 100% = 99.9%
+ </div>
+ </section>
+ </div>
+ <div class="col-lg-6 col-md-6 col-sm-6 ruletypes">
+ <section id="panel-2" class="panel panel-primary" (click)="click('pr')">
+ <div class="panel-heading">
+ <span style="font-size:20px">Data Profiling</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: Data profiling is the process of examining the data available in an existing data set and collecting statistics and information about that data</label>
+ <ul style="border-radius:0; background: none">
+ <li >
+ <a class="selected" style="cursor:default">
+ <div class="stepNumber">
+ 1
+ </div>
+ <span class="stepDesc text-small"> Choose Target </span>
+ </a>
+ </li>
+ <li>
+ <a style="cursor:default" >
+ <div class="stepNumber">
+ 2
+ </div>
+ <span class="stepDesc text-small">Define/Select Models </span>
+ </a>
+ </li>
+ <li >
+ <a style="cursor:default" >
+ <div class="stepNumber">
+ 3
+ </div>
+ <span class="stepDesc text-small"> Configuration </span>
+ </a>
+ </li>
+ </ul>
+ <div>
+ <ol>
+ <li>Select the target dataset and fields which want to be checked</li>
+ <li>Define your syntax check logic which will be applied on the selected fields</li>
+ <li>Set basic configuration for your model(name, system, threshold, etc.)</li>
+ </ol>
+ </div>
+ </div>
+ <div class="panel-footer stepDesc">
+ <label>Example:</label> Check the data range(minimum, maximum) within a set of allowable values
+ </div>
+ </section>
+ </div>
+ </div><!--//row-->
+</div>
http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/42ee8863/ui/angular/src/app/measure/create-measure/create-measure.component.spec.ts
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/measure/create-measure/create-measure.component.spec.ts b/ui/angular/src/app/measure/create-measure/create-measure.component.spec.ts
new file mode 100644
index 0000000..6e073a3
--- /dev/null
+++ b/ui/angular/src/app/measure/create-measure/create-measure.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 { CreateMeasureComponent } from './create-measure.component';
+
+describe('CreateMeasureComponent', () => {
+ let component: CreateMeasureComponent;
+ let fixture: ComponentFixture<CreateMeasureComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ CreateMeasureComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CreateMeasureComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/42ee8863/ui/angular/src/app/measure/create-measure/create-measure.component.ts
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/measure/create-measure/create-measure.component.ts b/ui/angular/src/app/measure/create-measure/create-measure.component.ts
new file mode 100644
index 0000000..ea89cf2
--- /dev/null
+++ b/ui/angular/src/app/measure/create-measure/create-measure.component.ts
@@ -0,0 +1,52 @@
+/*
+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, OnInit } from '@angular/core';
+import { FormControl} from '@angular/forms';
+import { FormsModule } from '@angular/forms';
+
+import { TREE_ACTIONS, KEYS, IActionMapping, ITreeOptions } from 'angular-tree-component';
+import { BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import { ToasterModule, ToasterService, ToasterConfig} from 'angular2-toaster';
+import * as $ from 'jquery';
+import { HttpClient} from '@angular/common/http';
+import { Router} from "@angular/router";
+
+@Component({
+ selector: 'app-create-measure',
+ templateUrl: './create-measure.component.html',
+ styleUrls: ['./create-measure.component.css']
+})
+export class CreateMeasureComponent implements OnInit {
+
+ constructor(private router:Router) { }
+
+
+ ngOnInit() {
+ $('#panel-2 >.panel-body').css({height: $('#panel-1 >.panel-body').outerHeight() + $('#panel-1 >.panel-footer').outerHeight() - $('#panel-2 >.panel-footer').outerHeight()});
+ $('#panel-4 >.panel-body').css({height: $('#panel-3 >.panel-body').outerHeight() + $('#panel-3 >.panel-footer').outerHeight() - $('#panel-4 >.panel-footer').outerHeight()});
+
+ }
+ click(type){
+ this.router.navigate(['/createmeasure'+type]);
+ }
+
+
+}
+
+
http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/42ee8863/ui/angular/src/app/measure/create-measure/pr/pr.component.css
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/measure/create-measure/pr/pr.component.css b/ui/angular/src/app/measure/create-measure/pr/pr.component.css
new file mode 100644
index 0000000..d7c4eb6
--- /dev/null
+++ b/ui/angular/src/app/measure/create-measure/pr/pr.component.css
@@ -0,0 +1,156 @@
+/*
+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');
+
+div.tree div.tree-children::before,
+div.tree::before {
+ content: "";
+ position: absolute;
+ border-left: 1px dotted #23527c;
+ height: 100%;
+ top: -14px;
+ left: 12px
+}
+
+tree-root{
+ color: #999;
+}
+
+div.tree {
+ padding-left: 0;
+ margin-left: -5px
+}
+
+div.tree div.tree-children {
+ position: relative;
+ padding-left: 0;
+ margin-left: 16px
+}
+
+div.tree div.tree-children::before {
+ left: 5px
+}
+
+div.tree treenode>div>.node-wrapper {
+ margin-left: 24px
+}
+
+div.tree treenode>div>.node-wrapper>.node-content-wrapper {
+ margin-left: 4px
+}
+
+div.tree treenode>div.tree-node-leaf>.node-wrapper {
+ margin-left: 0
+}
+
+div.tree treenode>div::before {
+ content: "";
+ position: absolute;
+ border-bottom: 1px dotted #23527c;
+ width: 7px;
+ margin-top: 12px;
+ left: 7px
+}
+
+div.tree treenode>div .toggle-children-wrapper {
+ width: 13px;
+ height: 13px;
+ border: 1px solid #23527c;
+ position: absolute;
+ left: 15px;
+ margin-top: 5px;
+ margin-left: 0;
+ display: inline-block;
+ background-color: #fff;
+ z-index: 1
+}
+
+div.tree treenode>div .toggle-children-wrapper::before {
+ content: "";
+ display: inline-block;
+ width: 7px;
+ border-top: 1px solid #23527c;
+ position: absolute;
+ top: 5px;
+ left: 2px
+}
+
+div.tree treenode>div .toggle-children-wrapper.toggle-children-wrapper-collapsed::after {
+ content: "";
+ display: inline-block;
+ height: 7px;
+ border-left: 1px solid #23527c;
+ position: absolute;
+ top: 2px;
+ left: 5px
+}
+
+div.tree treenode>div .toggle-children-wrapper .toggle-children {
+ display: none
+}
+
+div.tree treenode>div .node-content-wrapper {
+ margin-left: 4px
+}
+
+div.tree>treenode>div::before {
+ left: 14px
+}
+
+div.tree>treenode>div>.node-wrapper>treenodeexpander>.toggle-children-wrapper {
+ left: 22px
+}
+
+label{
+ font-weight: normal;
+}
+.container{
+ max-height: 40vh;
+ overflow-y:scroll;
+}
+
+.badgebox
+{
+ opacity: 0;
+}
+
+.badgebox + .badge
+{
+ /* Move the check mark away when unchecked */
+ text-indent: -999999px;
+ /* Makes the badge's width stay the same checked and unchecked */
+ width: 27px;
+}
+
+.badgebox:focus + .badge
+{
+ /* Set something to make the badge looks focused */
+ /* This really depends on the application, in my case it was: */
+
+ /* Adding a light border */
+ box-shadow: inset 0px 0px 5px;
+ /* Taking the difference out of the padding */
+}
+
+.badgebox:checked + .badge
+{
+ /* Move the check mark back when checked */
+ text-indent: 0;
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/42ee8863/ui/angular/src/app/measure/create-measure/pr/pr.component.html
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/measure/create-measure/pr/pr.component.html b/ui/angular/src/app/measure/create-measure/pr/pr.component.html
new file mode 100644
index 0000000..d667a80
--- /dev/null
+++ b/ui/angular/src/app/measure/create-measure/pr/pr.component.html
@@ -0,0 +1,392 @@
+<!--
+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">
+ <div class="row">
+ <h5 class="over-title margin-bottom-15">Create Measure</h5>
+ </div>
+ <div class="row">
+ <form name="Form" id="form" (ngSubmit)="submit(prForm)" #prForm="ngForm" novalidate>
+ <div id="wizard" class="swMain" >
+ <ul>
+ <li (click)="goTo(1)" >
+ <a [ngClass]="{'selected' : currentStep >= 1, 'done' : currentStep > 1}" class="selected">
+ <div class="stepNumber">
+ 1
+ </div>
+ <span class="stepDesc text-small"> Choose Source </span>
+ </a>
+ </li>
+ <li (click)="goTo(2)">
+ <a [ngClass]="{'selected' : currentStep >= 2, 'done' : currentStep > 2}" class="" style="">
+ <div class="stepNumber">
+ 2
+ </div>
+ <span class="stepDesc text-small"> Select Models </span>
+ </a>
+ </li>
+ <li (click)="goTo(3)">
+ <a [ngClass]="{'selected' : currentStep >= 3, 'done' : currentStep > 3}">
+ <div class="stepNumber">
+ 3
+ </div>
+ <span class="stepDesc text-small"> Configuration </span>
+ </a>
+ </li>
+ </ul>
+ </div>
+
+ <div id="step-1" *ngIf="currentStep == 1" class="formStep" >
+ <label class="stepDesc">This step let you choose the single source of truth for data quality comparision with target. Currently you can only select the attributes from one schema</label>
+ <div class="container-fluid">
+
+ <div class="col-md-4 col-lg-4 col-sm-4">
+ <fieldset>
+ <legend>Please select schema</legend>
+ <tree-root [nodes]="nodeList" [options]="options"></tree-root>
+ </fieldset>
+ </div>
+ <div class="col-md-8 col-lg-8 col-sm-8">
+ <fieldset>
+ <legend>
+ Select attributes
+ </legend>
+ <div class="y-scrollable">
+ <div>
+ <label>View schema:</label>
+ <i
+ style="color:#fff;font-weight: bold;">{{currentDB}}.{{currentTable}}
+ </i>
+ </div>
+ <div>
+ <table class="table table-striped">
+ <thead>
+ <tr style="background-color:#7D95CC">
+ <th><input type="checkbox" (click)="toggleAll()" [checked]="selectedAll"
+ /></th>
+ <th>Column Name</th>
+ <th>Type</th>
+ <th>Comment</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr *ngIf="!schemaCollection || schemaCollection.length == 0">
+ <td colspan="5" style="text-align:center" ><span class="highlight">Please select a schema from the left tree first</span></td>
+ </tr>
+ <tr *ngFor="let row of schemaCollection">
+ <td>
+ <input type="checkbox" (click)='toggleSelection(row)' [checked]="row.selected" value={{row.name}}
+ />
+ </td>
+ <td>{{row.name}}</td>
+ <td>{{row.type}}</td>
+ <td>{{row.comment}}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </fieldset>
+ </div>
+ <div class="form-group btn-container">
+ <toaster-container></toaster-container>
+ <button class="btn btn-primary btn-o next-step btn-wide pull-right" (click)="next(Form)">
+ Next <i class="fa fa-arrow-circle-right"></i>
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div id="step-2" *ngIf="currentStep == 2" class="formStep" >
+ <label class="stepDesc">Please choose one of the profiling models provided below:</label>
+ <div class="container-fluid">
+ <div class="col-md-12 col-lg-12 col-sm-12">
+ <fieldset>
+ <div class="y-scrollable">
+ <div class="container col-md-12 col-lg-12 col-sm-12">
+ <div class="row">
+ <div class="col-md-12 col-lg-12 col-sm-12" *ngFor="let item of selection;let itemIndex = index" >
+ <p style="font-weight: bold;font-size: 120%;cursor:pointer"
+ (click)="item.isExpanded = !item.isExpanded">
+ <i *ngIf="!item.isExpanded" class="fa fa-chevron-circle-right blue"></i>
+ <i *ngIf="item.isExpanded" class="fa fa-chevron-circle-down blue"></i> {{currentDB}}.{{currentTable}}.{{item.name}}
+ <i>:{{item.ruleLength}} rules</i>
+ </p>
+ <div *ngIf="item.isExpanded">
+ <div id="simple{{itemIndex}}">
+ <h5>Simple Statistics</h5>
+ <div *ngFor="let rule of item.newRules;let ruleIndex = index" >
+ <div *ngIf="rule!=null">
+ <a class="btn btn-primary btn-o" style="background-color: #337ab7;"
+ (click)="removeRule(item,ruleIndex)">
+ <i class="fa fa-times"></i>
+ </a>
+ <label style="width: 7%;text-align:center;">Rule:</label>
+ <select class="form-control" style="width: 25%;display: inline-block;"
+ id="simpleRule" name='simpleRule-{{itemIndex}}{{ruleIndex}}' [(ngModel)]='rule.type'>
+ <option>Total Count</option>
+ <option>Distinct Count</option>
+ <option>Null Detection Count</option>
+ <option *ngIf="item.type=='string'||item.type=='varchar'||item.type=='char'">Regular Expression Detection Count</option>
+ <option>Rule Detection Count</option>
+ <option *ngIf='item.isNum'>Maximum</option>
+ <option *ngIf='item.isNum'>Minimum</option>
+ <option *ngIf='item.isNum'>Median</option>
+ <option *ngIf='item.isNum'>Average</option>
+ <option>Enum Detection Count</option>
+ </select>
+ <!-- <a class="btn btn-primary btn-o" style="background-color: #337ab7;"
+ (click)="addCond(item,ruleIndex)">
+ <i class="fa fa-plus"></i>
+ </a> -->
+ <div style="display: inline-block;font-size: 130%;vertical-align: middle;" *ngFor="let cond of rule.conditionGroup;let condIndex=index">
+ <input type="checkbox"
+ name="condition-{{itemIndex}}{{ruleIndex}}{{condIndex}}"
+ value="cond.type"
+ (click)='toggleSelectionCond(cond,condIndex,ruleIndex,item)'
+ [disabled]="!cond.avaliable" [(ngModel)]="cond.chosen" style="width:18px;height: 18px;">
+ <!-- <span class="badge">✓</span> -->
+ <label>{{cond.type}}</label>
+ <br>
+ </div>
+ <div *ngFor="let cond of rule.conditionGroup;let condIndex=index" style="margin-left:12%;">
+ <label style="width: 7%;" *ngIf="cond.chosen">{{cond.type}}</label>
+ <input *ngIf="cond.chosen"
+ name="conditionDetail-{{itemIndex}}{{ruleIndex}}{{condIndex}}" type="text" class="form-control" style="width: 40%;display: inline-block;" [(ngModel)]="cond.content"/>
+ </div>
+
+ </div>
+
+ </div>
+
+ <a class="btn btn-primary btn-o" style="background-color: #337ab7;"
+ (click)="addRule(item)">
+ <i class="fa fa-plus"></i>
+ </a>
+ <br>
+ </div>
+ </div>
+ </div>
+ <!-- <div class="col-md-9 col-lg-9 col-sm-9" ng-include="'/pages/rules/va-explaination.html'">
+ </div> -->
+ </div>
+ </div>
+ </div>
+ </fieldset>
+ </div>
+ <div class="form-group btn-container" >
+ <button class="btn btn-primary btn-o back-step btn-wide pull-left" (click)="prev(Form)">
+ <i class="fa fa-arrow-circle-left"></i> Back
+ </button>
+ <toaster-container></toaster-container>
+ <button class="btn btn-primary btn-o next-step btn-wide pull-right" (click)="next(Form)">
+ Next <i class="fa fa-arrow-circle-right"></i>
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div id="step-3" *ngIf="currentStep == 3" 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">
+ <div class="form-group" [ngClass]="{'has-error':prName.dirty&&prName.invalid, 'has-success':prName.valid}">
+ <label class="col-md-2 col-lg-2 col-sm-2 control-label">
+ Measure Name<span class="symbol required"></span>:
+ </label>
+ <div class="col-md-10 col-lg-10 col-sm-10 ">
+ <input type="text" class="form-control" [(ngModel)]="name" #prName="ngModel" name="prName" placeholder="Please input the measure name" required pattern="^[a-zA-Z0-9_-]*$">
+ <span class="error text-small block " *ngIf="prName.dirty && (prName.errors?.required)">Measure Name is required</span>
+ <span class="error text-small block " *ngIf="prName.dirty && (prName.errors?.pattern)">Only letter, number, "-" and "_" are allowed</span>
+ </div>
+ </div>
+ </div>
+ <div class="col-md-12 col-lg-12 col-sm-12">
+ <div class="form-group">
+ <label for="typeSelector" class="col-md-2 col-lg-2 col-sm-2 control-label">
+ Measure Type:
+ </label>
+ <div class="col-md-10 col-lg-10 col-sm-10 ">
+ <select id="typeSelector" class="form-control" [(ngModel)]="type" disabled required name="type">
+ <option>{{type}}</option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <div class="col-md-12 col-lg-12 col-sm-12">
+ <div class="form-group">
+ <label for="systemSelector" class="col-md-2 col-lg-2 col-sm-2 control-label">
+ Organization:
+ </label>
+ <div class="col-md-10 col-lg-10 col-sm-10 ">
+ <input type="text" id="systemSelector" class="form-control" required ng-pattern="'([0-9a-zA-Z\\_\\-])+'" name="org" value="{{currentDB}}" disabled>
+ </div>
+ </div>
+ </div>
+ <div class="col-md-12 col-lg-12 col-sm-12">
+ <div class="form-group">
+ <label class="col-md-2 col-lg-2 col-sm-2 control-label">
+ DataAsset:
+ </label>
+ <div class="col-md-10 col-lg-10 col-sm-10">
+ <input type="text" class="form-control" name="DataAsset"
+ value="{{currentTable}}" disabled>
+ </div>
+ </div>
+ </div>
+ <!-- <div class="col-md-12 col-lg-12 col-sm-12">
+ <div class="form-group">
+ <label class="col-md-2 col-lg-2 col-sm-2 control-label">
+ Owner:
+ </label>
+ <div class="col-md-10 col-lg-10 col-sm-10">
+ <input type="text" class="form-control" name="owner" disabled
+ [(ngModel)]="owner" >
+ </div>
+ </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>
+ </fieldset>
+ </div>
+
+ <div class="form-group btn-container" >
+ <button class="btn btn-primary btn-o back-step btn-wide pull-left" (click)="prev(Form)">
+ <i class="fa fa-arrow-circle-left"></i> Back
+ </button>
+ <toaster-container></toaster-container>
+ <button type="submit" 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" data-dismiss="modal" aria-hidden="true">×</button>
+ <h4 class="modal-title">Save the measure with the below information?</h4>
+ </div>
+ <div class="modal-body">
+ <div class="container-fluid" id="viewruleContent" style="overflow:auto;">
+ <div class="row">
+ <h5 class="over-title margin-bottom-15">Basic information</h5>
+ </div>
+ <div class="row">
+ <div class="col-lg-12 col-md-12 col-sm-12">
+ <div id="viewrule-definition" class="viewrule-content">
+ <div class="row">
+ <label class="col-md-4 col-lg-4 col-sm-4">
+ Measure Name:
+ </label>
+ <div class="col-md-8 col-lg-8 col-sm-8 ">
+ {{name}}
+ </div>
+ </div>
+ <div class="row">
+ <label class="col-md-4 col-lg-4 col-sm-4">
+ Measure Type:
+ </label>
+ <div class="col-md-8 col-lg-8 col-sm-8 ">
+ {{type}}
+ </div>
+ </div>
+ <div class="row">
+ <label for="systemSelector" class="col-md-4 col-lg-4 col-sm-4">
+ Organization:
+ </label>
+ <div class="col-md-8 col-lg-8 col-sm-8 ">
+ {{currentDB}}
+ </div>
+ </div>
+ <div class="row">
+ <label class="col-md-4 col-lg-4 col-sm-4">
+ DataAsset:
+ </label>
+ <div class="col-md-8 col-lg-8 col-sm-8">
+ {{currentTable}}
+ </div>
+ </div>
+<!-- <div class="row">
+ <label class="col-md-4 col-lg-4 col-sm-4">
+ Owner:
+ </label>
+ <div class="col-md-8 col-lg-8 col-sm-8" >
+ {{owner}}
+ </div>
+ </div> -->
+
+ </div>
+ </div>
+ </div>
+ <h5 class="row">Rules</h5>
+ <div class="row" *ngFor = "let newRule of newMeasure.evaluateRule.rules">
+ {{newRule.rule}}
+ </div>
+ <br/>
+ <!-- <h5 class="row">Mapping rules</h5> -->
+ <!-- <div class="row">
+ <p>{{rules}}</p>
+ <p>
+ <label style="color:#B2C831">Accuracy Calculation Formula as Below:</label>
+ </p>
+ <div class="col-md-12 col-lg-12 col-sm-12" style="color:#fff;font-size:16px;display: flex; align-items: center">
+ <div class="" style="text-align:right;display:block;float:left;width:20%;">
+ Accuracy Rate(%) =
+ </div>
+ <div class="" style="text-align:center;display:block;float:left;margin:0 10px 0 10px">
+ <div class="formula-text-up" style="border-bottom:1px solid;">
+ Total Count of Matched records between <span class="badge">{{selectionTarget.length}}</span> <span style="color:green;">{{currentTableTarget}}</span> and <span class="badge">{{mappings.length}}
+ </span> <span style="color:green;">{{currentTable}}</span> fields
+ </div>
+ <div>
+ Total Count of records in <span style="color:green;font-weight:bold;">{{currentDB}}.{{currentTable}}</span>
+ </div>
+ </div>
+ <div class="" style="text-align:left;display:block;float:left;width:10%;">
+ x 100%
+ </div>
+ </div>
+ </div> -->
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" (click)="hide()">Cancel</button>
+ <button type="button" 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/42ee8863/ui/angular/src/app/measure/create-measure/pr/pr.component.spec.ts
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/measure/create-measure/pr/pr.component.spec.ts b/ui/angular/src/app/measure/create-measure/pr/pr.component.spec.ts
new file mode 100644
index 0000000..dcc2d00
--- /dev/null
+++ b/ui/angular/src/app/measure/create-measure/pr/pr.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 { PrComponent } from './pr.component';
+
+describe('PrComponent', () => {
+ let component: PrComponent;
+ let fixture: ComponentFixture<PrComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ PrComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PrComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/42ee8863/ui/angular/src/app/measure/create-measure/pr/pr.component.ts
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/measure/create-measure/pr/pr.component.ts b/ui/angular/src/app/measure/create-measure/pr/pr.component.ts
new file mode 100644
index 0000000..4e80080
--- /dev/null
+++ b/ui/angular/src/app/measure/create-measure/pr/pr.component.ts
@@ -0,0 +1,580 @@
+/*
+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, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
+import { FormsModule } from '@angular/forms';
+import {ServiceService} from '../../../service/service.service';
+import { TREE_ACTIONS, KEYS, IActionMapping, ITreeOptions } from 'angular-tree-component';
+import { BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import { ToasterModule, ToasterService,ToasterContainerComponent} from 'angular2-toaster';
+import * as $ from 'jquery';
+import { HttpClient} from '@angular/common/http';
+import { Router} from "@angular/router";
+
+
+class node {
+ name: string;
+ id: number;
+ children:object[];
+ isExpanded:boolean;
+ cols:Col[];
+ parent:string;
+};
+
+class Rule{
+ type:string;
+ conditionGroup = [
+ {
+ 'type':'where',
+ 'content':'',
+ 'chosen':false,
+ 'avaliable':true
+ },
+ {
+ 'type':'groupby',
+ 'content':'',
+ 'chosen':false,
+ 'avaliable':true
+ },
+ {
+ 'type':'having',
+ 'content':'',
+ 'chosen':false,
+ 'avaliable':false
+ },
+ {
+ 'type':'orderby',
+ 'content':'',
+ 'chosen':false,
+ 'avaliable':true
+ },
+ {
+ 'type':'limit',
+ 'content':'',
+ 'chosen':false,
+ 'avaliable':true
+ }
+ ];
+}
+
+class Col{
+ name:string;
+ type:string;
+ comment:string;
+ selected :boolean;
+ isNum:boolean;
+ isExpanded:boolean;
+ // rules:string[];
+ groupby:string;
+ RE:string;
+ newRules:Rule[];
+ ruleLength = 0;
+ constructor(name:string,type:string,comment:string,selected:boolean){
+ this.name = name;
+ this.type = type;
+ this.comment = comment;
+ this.selected = false;
+ this.isExpanded = false;
+ this.groupby = '';
+ this.RE = '';
+ this.newRules = [
+ ];
+
+ var patt = new RegExp('int|double|float/i');
+ if(patt.test(this.type)){
+ this.isNum = true;
+ }
+ // this.rules = [];
+ }
+}
+
+@Component({
+ selector: 'app-pr',
+ templateUrl: './pr.component.html',
+ providers:[ServiceService],
+ styleUrls: ['./pr.component.css']
+})
+export class PrComponent implements OnInit {
+
+ currentStep = 1;
+ firstCond = false;
+ selection : Col[];
+ selectedAll = false;
+ rules = '';
+ currentDB = '';
+ currentTable = '';
+ schemaCollection:Col[];
+ totallen = 0;
+ type = 'profiling';
+ data:any;
+ newMeasure = {
+ "name": "",
+ "process.type": "batch",
+ "data.sources": [
+ {
+ "name": "source",
+ "connectors": [
+ {
+ "type": "hive",
+ "version": "1.2",
+ "config": {
+ "database": "",
+ "table.name":""
+ }
+ }
+ ]
+ }
+ ],
+ "evaluateRule": {
+ "rules": [
+ {
+ "dsl.type": "griffin-dsl",
+ "dq.type": "profiling",
+ "rule": ""
+ // "details": {}
+ }
+ ]
+ }
+ };
+ name:'';
+ createResult :any;
+ newCond:any;
+
+ private toasterService: ToasterService;
+ public visible = false;
+ public visibleAnimate = false;
+
+ public hide(): void {
+ this.visibleAnimate = false;
+ setTimeout(() => this.visible = false, 300);
+ }
+
+ public onContainerClicked(event: MouseEvent): void {
+ if ((<HTMLElement>event.target).classList.contains('modal')) {
+ this.hide();
+ }
+ }
+
+ toggleSelectionCond(cond,condIndex,ruleIndex,item){
+ cond.chosen = !cond.chosen;
+ if(condIndex==1&&cond.chosen)
+ item.newRules[ruleIndex].conditionGroup[2].avaliable = true;
+ if(condIndex==1&&!cond.chosen){
+ item.newRules[ruleIndex].conditionGroup[2].avaliable = false;
+ item.newRules[ruleIndex].conditionGroup[2].chosen = false;
+ }
+ }
+
+ toggleSelection (row) {
+ row.selected = !row.selected;
+ console.log(row);
+ var idx = this.selection.indexOf(row);
+ // is currently selected
+ if (idx > -1) {
+ this.selection.splice(idx, 1);
+ this.selectedAll = false;
+ }
+ // is newly selected
+ else {
+ this.selection.push(row);
+ }
+ };
+
+ toggleAll () {
+ this.selectedAll = !this.selectedAll;
+ this.selection = [];
+ for(var i =0; i < this.schemaCollection.length; i ++){
+ this.schemaCollection[i].selected = this.selectedAll;
+ if (this.selectedAll) {
+ this.selection.push(this.schemaCollection[i]);
+ }
+ }
+ };
+
+ transferRule(rule,col){
+ switch(rule){
+ case 'Total Count':
+ return 'count(source.'+col.name+') ';
+ case 'Distinct Count':
+ return 'distinct count(source.'+col.name+') ';
+ case 'Null Detection Count':
+ return 'count(source.'+col.name+') where source.'+col.name+' is null';
+ case 'Regular Expression Detection Count':
+ return 'count(source.'+col.name+') where source.'+col.name+' like ';
+ case 'Rule Detection Count':
+ return 'count(source.'+col.name+') where source.'+col.name+' like ';
+ case 'Maxium':
+ return 'max(source.'+col.name+') ';
+ case 'Minimum':
+ return 'min(source.'+col.name+') ';
+ case 'Median':
+ return 'median(source.'+col.name+') ';
+ case 'Average':
+ return 'average(source.'+col.name+') ';
+ case 'Enum Detection Count':
+ return 'source.'+col.name+' group by source.'+col.name+'';
+ // case 'Groupby Count':
+ // return 'source.'+col.name+' group by source.'+col.name+'';
+ // case 'total count':
+ // return 'SELECT COUNT(*) FROM source';
+ // case 'distinct count':
+ // return 'SELECT DISTINCT COUNT(source.'+col.name+') FROM source';
+ // case 'null detection count':
+ // return 'SELECT COUNT(source.'+col.name+') FROM source WHERE source.'+col.name+' is null';
+ // case 'regular expression detection count':
+ // return 'SELECT COUNT(source.'+col.name+') FROM source WHERE source.'+col.name+' like '+col.RE;
+ // case 'rule detection count':
+ // return 'SELECT COUNT(source.'+col.name+') FROM source WHERE source.'+col.name+' like ';
+ // case 'max':
+ // return 'SELECT max(source.'+col.name+') FROM source';
+ // case 'min':
+ // return 'SELECT min(source.'+col.name+') FROM source';
+ // case 'median':
+ // return 'SELECT median(source.'+col.name+') FROM source';
+ // case 'avg':
+ // return 'SELECT average(source.'+col.name+') FROM source';
+ // case 'enum detection group count':
+ // return 'source.'+col.name+' group by source.'+col.name+'';
+ // case 'groupby count':
+ // return 'source.'+col.name+' group by source.'+col.name+' '+col.groupby;
+ }
+ }
+
+ addCond(item,ruleIndex){ }
+
+ addRule(item){
+ item.ruleLength++;
+ let newRule = new Rule();
+ item.newRules.push(newRule);
+ }
+
+ removeRule(item,ruleIndex){
+ item.ruleLength--;
+ item.newRules[ruleIndex] = null;
+ }
+
+ next (form) {
+ if(this.formValidation(this.currentStep)){
+ this.currentStep++;
+ }else{
+ this.toasterService.pop('error','Error!','Please select at least one attribute!');
+ return false;
+ }
+ }
+
+ formValidation = function(step) {
+ if (step == undefined) {
+ step = this.currentStep;
+ }
+ if (step == 1) {
+ return this.selection && this.selection.length > 0;
+ } else if (step == 2) {
+ for(let item of this.selection){
+ this.totallen = this.totallen + item.newRules.length;
+ }
+ return (this.totallen > 0)
+ } else if (step == 3) {
+ }
+ return false;
+ }
+
+ prev (form) {
+ this.currentStep--;
+ }
+ goTo (i) {
+ this.currentStep = i;
+ }
+ submit (form) {
+ // form.$setPristine();
+ // if (!form.valid) {
+ // this.toasterService.pop('error', 'Error!', 'please complete the form in this step before proceeding');
+ // return false;
+ // }
+ this.newMeasure = {
+ "name": this.name,
+ "process.type": "batch",
+ "data.sources": [
+ {
+ "name": "source",
+ "connectors": [
+ {
+ "type": "hive",
+ "version": "1.2",
+ "config": {
+ "database": this.currentDB,
+ "table.name":this.currentTable
+ }
+ }
+ ]
+ }
+ ],
+ "evaluateRule": {
+ "rules": [
+ {
+ "dsl.type": "griffin-dsl",
+ "dq.type": "profiling",
+ "rule": ""
+ // "details": {}
+ }
+ ]
+ }
+ };
+
+ var self = this;
+ var rule = '';
+ for(let item of this.selection){
+ for(let itemRule of item.newRules){
+ console.log(self.transferRule(itemRule.type,item));
+ if(itemRule.conditionGroup[0].chosen==true){
+ let whereRule = self.transferRule(itemRule.type,item);
+ for(let condition of itemRule.conditionGroup){
+ if(condition.content!='')
+ whereRule = whereRule + condition.type + ' ' + condition.content + ',';
+ }
+ self.newMeasure.evaluateRule.rules.push({
+ "dsl.type": "griffin-dsl",
+ "dq.type": "profiling",
+ "rule": whereRule,
+ // "details": {}
+ });
+ }
+ else {
+ let normalRule = self.transferRule(itemRule.type,item);
+ for(let condition of itemRule.conditionGroup){
+ if(condition.content!='')
+ normalRule = normalRule + ' '+ condition.type + ' ' + condition.content + ',';
+ }
+ rule = rule + normalRule;
+ }
+ }
+ }
+ // this.newMeasure.evaluateRule.rules[0].rule = rule;
+ self.newMeasure.evaluateRule.rules.push({
+ "dsl.type": "griffin-dsl",
+ "dq.type": "profiling",
+ "rule": rule,
+ // "details": {}
+ });
+ this.visible = true;
+ setTimeout(() => this.visibleAnimate = true, 100);
+ }
+
+ save() {
+ console.log(this.newMeasure);
+ var addModels = this.servicecService.config.uri.addModels;
+ this.http
+ .post(addModels, this.newMeasure)
+ .subscribe(data => {
+ this.createResult = data;
+ this.hide();
+ this.router.navigate(['/measures']);
+ },
+ err => {
+ console.log('Something went wrong!');
+ });
+ }
+
+ // data: { [key: string]: Array<object>; } = {
+ // "default": [
+ // {
+ // "tableName": "ext",
+ // "dbName": "default",
+ // "owner": "hadoop",
+ // "createTime": 1488353464,
+ // "lastAccessTime": 0,
+ // "retention": 0,
+ // "sd": {
+ // "cols": [
+ // {
+ // "name": "id",
+ // "type": "int",
+ // "comment": null,
+ // "setName": true,
+ // "setComment": false,
+ // "setType": true
+ // },
+ // {
+ // "name": "name",
+ // "type": "string",
+ // "comment": null,
+ // "setName": true,
+ // "setComment": false,
+ // "setType": true
+ // },
+ // {
+ // "name": "age",
+ // "type": "int",
+ // "comment": null,
+ // "setName": true,
+ // "setComment": false,
+ // "setType": true
+ // }
+ // ],
+ // "location": "hdfs://10.9.246.187/user/hive/ext",
+ // },
+ // },
+ // {
+ // "tableName": "ext1",
+ // "dbName": "default",
+ // "owner": "hadoop",
+ // "createTime": 1489382943,
+ // "lastAccessTime": 0,
+ // "retention": 0,
+ // "sd": {
+ // "cols": [
+ // {
+ // "name": "id",
+ // "type": "int",
+ // "comment": null,
+ // "setName": true,
+ // "setComment": false,
+ // "setType": true
+ // },
+ // {
+ // "name": "name",
+ // "type": "string",
+ // "comment": null,
+ // "setName": true,
+ // "setComment": false,
+ // "setType": true
+ // },
+ // {
+ // "name": "age",
+ // "type": "int",
+ // "comment": null,
+ // "setName": true,
+ // "setComment": false,
+ // "setType": true
+ // }
+ // ],
+ // "location": "hdfs://10.9.246.187/user/hive/ext1",
+ // },
+ // }
+ // ],
+ // "griffin": [
+ // {
+ // "tableName": "avr_out",
+ // "dbName": "griffin",
+ // "owner": "hadoop",
+ // "createTime": 1493892603,
+ // "lastAccessTime": 0,
+ // "retention": 0,
+ // "sd": {
+ // "cols": [
+ // {
+ // "name": "id",
+ // "type": "bigint",
+ // "comment": null,
+ // "setName": true,
+ // "setComment": false,
+ // "setType": true
+ // },
+ // {
+ // "name": "age",
+ // "type": "int",
+ // "comment": null,
+ // "setName": true,
+ // "setComment": false,
+ // "setType": true
+ // },
+ // {
+ // "name": "desc",
+ // "type": "string",
+ // "comment": null,
+ // "setName": true,
+ // "setComment": false,
+ // "setType": true
+ // }
+ // ],
+ // "location": "hdfs://10.9.246.187/griffin/data/batch/avr_out",
+ // },
+ // }
+ // ],
+ // };
+
+ options: ITreeOptions = {
+ displayField: 'name',
+ isExpandedField: 'expanded',
+ idField: 'id',
+ actionMapping: {
+ mouse: {
+ click: (tree, node, $event) => {
+ if (node.hasChildren) {
+ this.currentDB = node.data.name;
+ this.currentTable = '';
+ TREE_ACTIONS.TOGGLE_EXPANDED(tree, node, $event);
+ }
+ else if(node.data.cols)
+ {
+ this.currentTable = node.data.name;
+ this.currentDB = node.data.parent;
+ this.schemaCollection = node.data.cols;
+ }
+ }
+ }
+ },
+ animateExpand: true,
+ animateSpeed: 30,
+ animateAcceleration: 1.2
+ };
+
+ nodeList:object[];
+ nodeListTarget:object[];
+
+ constructor(toasterService: ToasterService,private http: HttpClient,private router:Router,public servicecService:ServiceService) {
+ this.toasterService = toasterService;
+ this.selection = [];
+ };
+
+
+ ngOnInit() {
+ var allDataassets = this.servicecService.config.uri.dataassetlist;
+ this.http.get(allDataassets).subscribe(data =>{
+ this.nodeList = new Array();
+ let i = 1;
+ this.data = data;
+ for (let db in this.data) {
+ let new_node = new node();
+ new_node.name = db;
+ new_node.id = i;
+ new_node.isExpanded = true;
+ i++;
+ new_node.children = new Array();
+ for(let i = 0;i<this.data[db].length;i++){
+ let new_child = new node();
+ new_child.name = this.data[db][i]['tableName'];
+ new_node.children.push(new_child);
+ new_child.isExpanded = false;
+ new_child.parent = db;
+ new_child.cols = Array<Col>();
+ for(let j = 0;j<this.data[db][i]['sd']['cols'].length;j++){
+ let new_col = new Col(this.data[db][i]['sd']['cols'][j].name,
+ this.data[db][i]['sd']['cols'][j].type,
+ this.data[db][i]['sd']['cols'][j].comment,false);
+ new_child.cols.push(new_col);
+ }
+ }
+ this.nodeList.push(new_node);
+ }
+ this.nodeListTarget = JSON.parse(JSON.stringify(this.nodeList));
+
+ });
+
+ };
+}
http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/42ee8863/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
new file mode 100644
index 0000000..214f8c6
--- /dev/null
+++ b/ui/angular/src/app/measure/measure-detail/measure-detail.component.css
@@ -0,0 +1,18 @@
+/*
+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.
+*/
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/42ee8863/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
new file mode 100644
index 0000000..4552627
--- /dev/null
+++ b/ui/angular/src/app/measure/measure-detail/measure-detail.component.html
@@ -0,0 +1,105 @@
+<!--
+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" id="viewruleContent">
+ <div class="row">
+ <h5 class="over-title margin-bottom-15">View Measure</h5>
+ </div><!--//row-->
+ <div class="row">
+ <div class="col-sm-6 col-xs-12">
+ <div id="viewruleDefinition" class="viewrule-content">
+ <div class="row">
+ <label class="col-xs-4">
+ Measure Name:
+ </label>
+ <div class="col-xs-8 " style="color: #fff">
+ {{ruleData.name}}
+ </div>
+ </div>
+ <div class="row">
+ <label class="col-xs-4">
+ Measure Description:
+ </label>
+ <div class="col-xs-8 " style="color: #fff">
+ {{ruleData.description}}
+ </div>
+ </div>
+ <div class="row">
+ <label class="col-xs-4">
+ Measure Type:
+ </label>
+ <div class="col-xs-8 " style="color: #fff">
+ {{ruleData.type}}
+ </div>
+ </div>
+ <div class="row">
+ <label for="systemSelector" class="col-xs-4">
+ Organization:
+ </label>
+ <div class="col-xs-8 " style="color: #fff">
+ {{ruleData.organization}}
+ </div>
+ </div>
+ <div class="row">
+ <label class="col-xs-4">
+ DataAsset:
+ </label>
+ <div class="col-xs-8" style="color: #fff">
+ {{sourceTable}} {{targetTable}}
+ </div>
+ </div>
+ <div class="row">
+ <label class="col-xs-4">
+ Owner:
+ </label>
+ <div class="col-xs-8" style="color: #fff">
+ {{ruleData.owner}}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div><!--//row-->
+ <br/>
+ <div ng-if="ruleData.type=='accuracy'"><!--Accuracy-->
+ <h5 class="row">Mapping rules</h5>
+ <div class="">
+ <p>{{ruleData.evaluateRule.rules}}</p>
+ <p>
+ <label style="color:#B2C831">Accuracy Calculation Formula as Below:</label>
+ </p>
+ <div class="col-md-12 col-lg-12 col-sm-12" style="color:#fff;font-size:16px;display: flex;align-items: center">
+ <div class="" style="text-align:right;display:block;float:left;width:20%;">
+ Accuracy Rate(%) =
+ </div>
+ <div class="" style="text-align:center;display:block;float:left;margin:0 10px 0 10px">
+ <div class="formula-text-up" style="border-bottom:1px solid;">
+ Total Count of Matched records between <span class="badge">{{sourceLength}}</span> <span style="color:green;">{{targetTable}}</span> and <span class="badge">{{sourceLength}}</span> <span style="color:green;">{{sourceTable}}</span> fields
+ </div>
+ <div class="">
+ Total Count of records in <span style="color:green;font-weight:bold;">{{targetDB}} {{targetTable}}</span>
+ </div>
+ </div>
+ <div class="" style="text-align:left;display:block;float:left;width:10%;">
+ x 100%
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/42ee8863/ui/angular/src/app/measure/measure-detail/measure-detail.component.spec.ts
----------------------------------------------------------------------
diff --git a/ui/angular/src/app/measure/measure-detail/measure-detail.component.spec.ts b/ui/angular/src/app/measure/measure-detail/measure-detail.component.spec.ts
new file mode 100644
index 0000000..4a5d8a2
--- /dev/null
+++ b/ui/angular/src/app/measure/measure-detail/measure-detail.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 { MeasureDetailComponent } from './measure-detail.component';
+
+describe('MeasureDetailComponent', () => {
+ let component: MeasureDetailComponent;
+ let fixture: ComponentFixture<MeasureDetailComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ MeasureDetailComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MeasureDetailComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});