diff options
| author | Alex Tatiyants <atatiyan@gmail.com> | 2016-01-03 17:17:48 -0800 | 
|---|---|---|
| committer | Alex Tatiyants <atatiyan@gmail.com> | 2016-01-03 17:17:48 -0800 | 
| commit | 5310ac7d8eb1838a6297117bc7f9fca70291f46a (patch) | |
| tree | 28f54b184cb85f04e6d6720dd03258f3728fedde /app/components | |
initial commit
Diffstat (limited to 'app/components')
| -rw-r--r-- | app/components/app/app.html | 6 | ||||
| -rw-r--r-- | app/components/app/app.ts | 22 | ||||
| -rw-r--r-- | app/components/plan-list/plan-list.html | 38 | ||||
| -rw-r--r-- | app/components/plan-list/plan-list.ts | 47 | ||||
| -rw-r--r-- | app/components/plan-new/plan-new.html | 18 | ||||
| -rw-r--r-- | app/components/plan-new/plan-new.ts | 26 | ||||
| -rw-r--r-- | app/components/plan-node/plan-node.html | 50 | ||||
| -rw-r--r-- | app/components/plan-node/plan-node.ts | 176 | ||||
| -rw-r--r-- | app/components/plan-view/plan-view.html | 72 | ||||
| -rw-r--r-- | app/components/plan-view/plan-view.ts | 96 | 
10 files changed, 551 insertions, 0 deletions
| diff --git a/app/components/app/app.html b/app/components/app/app.html new file mode 100644 index 0000000..a8a1969 --- /dev/null +++ b/app/components/app/app.html @@ -0,0 +1,6 @@ +<router-outlet></router-outlet> +<footer>PEV is made by  +   <a href="http://tatiyants.com/">Alex Tatiyants</a> +   <a href="https://twitter.com/AlexTatiyants"><i class="fa fa-twitter"></i></a> +   <a href="https://github.com/AlexTatiyants"><i class="fa fa-github"></i></a> +</footer> diff --git a/app/components/app/app.ts b/app/components/app/app.ts new file mode 100644 index 0000000..f9c83e0 --- /dev/null +++ b/app/components/app/app.ts @@ -0,0 +1,22 @@ +import {Component, ViewEncapsulation} from 'angular2/core'; +import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router'; + +import {PlanView} from '../plan-view/plan-view'; +import {PlanList} from '../plan-list/plan-list'; +import {PlanNew} from '../plan-new/plan-new'; + +@Component({ +    selector: 'app', +    templateUrl: './components/app/app.html', +    encapsulation: ViewEncapsulation.None, +    directives: [ROUTER_DIRECTIVES] +}) + +@RouteConfig([ +    { path: '/', redirectTo: ['/PlanList'] }, +    { path: '/plans', component: PlanList, name: 'PlanList' }, +    { path: '/plans/new', component: PlanNew, name: 'PlanNew' }, +    { path: '/plans/:id', component: PlanView, name: 'PlanView' } +]) + +export class App { } diff --git a/app/components/plan-list/plan-list.html b/app/components/plan-list/plan-list.html new file mode 100644 index 0000000..a2f1a9d --- /dev/null +++ b/app/components/plan-list/plan-list.html @@ -0,0 +1,38 @@ +<nav> +   <div class="nav-container"> +   <a class="btn btn-primary btn-lg pull-right" [routerLink]="['/PlanNew']">create</a> +   <a [routerLink]="['PlanList']">plans</a> +   </div> +</nav> + +<div class="page"> +   <div class="hero-container" *ngIf="plans.length === 0"> +      Welcome to PEV! Please <a [routerLink]="['/PlanNew']">submit</a> a plan for visualization +   </div> + +  <table class="table"> +    <tr *ngFor="#plan of plans"> +      <td><a [routerLink]="['/PlanView', {id: plan.id}]">{{plan.name}}</a></td> +      <td>created on {{plan.createdOn | momentDate }}</td> +      <td class="align-right"><button class="btn btn-danger" (click)="requestDelete()"> +        <i class="fa fa-trash"></i>delete</button> +        <!-- this is a hack that should be converted to a proper dialog once that is available in angular 2--> +        <div *ngIf="openDialog"> +           <div class="modal-backdrop"></div> + +           <div class="modal"> +              <div class="modal-dialog"> +                 <div class="modal-content"> +                    <div class="modal-body">You're about to delete this plan. Are you sure?</div> +                    <div class="modal-footer"> +                       <button class="btn btn-primary" (click)="deletePlan(plan)">Yes</button> +                       <button class="btn btn-default" (click)="cancelDelete()">No</button> +                    </div> +                </div> +              </div> +           </div> +        </div> +     </td> +    </tr> +  </table> +</div> diff --git a/app/components/plan-list/plan-list.ts b/app/components/plan-list/plan-list.ts new file mode 100644 index 0000000..888e2e2 --- /dev/null +++ b/app/components/plan-list/plan-list.ts @@ -0,0 +1,47 @@ +import {Component, OnInit} from 'angular2/core'; +import {ROUTER_DIRECTIVES} from 'angular2/router'; + +import {IPlan} from '../../interfaces/iplan'; +import {PlanService} from '../../services/plan-service'; +import {PlanNew} from '../plan-new/plan-new'; + +import {MomentDatePipe} from '../../pipes'; + +@Component({ +    selector: 'plan-list', +    templateUrl: './components/plan-list/plan-list.html', +    providers: [PlanService], +    directives: [ROUTER_DIRECTIVES, PlanNew], +    pipes: [MomentDatePipe] +}) +export class PlanList { +    plans: Array<IPlan>; +    newPlanName: string; +    newPlanContent: any; +    newPlanId: string; +    openDialog: boolean = false; + +    constructor(private _planService: PlanService) { } + +    ngOnInit() { +        this.plans = this._planService.getPlans(); +    } + +    requestDelete() { +        this.openDialog = true; +    } + +    deletePlan(plan) { +        this.openDialog = false; +        this._planService.deletePlan(plan); +        this.plans = this._planService.getPlans(); +    } + +    cancelDelete() { +        this.openDialog = false; +    } + +    deleteAllPlans() { +        this._planService.deleteAllPlans(); +    } +} diff --git a/app/components/plan-new/plan-new.html b/app/components/plan-new/plan-new.html new file mode 100644 index 0000000..f9babb6 --- /dev/null +++ b/app/components/plan-new/plan-new.html @@ -0,0 +1,18 @@ +<nav> +   <div class="nav-container"> +      <a [routerLink]="['PlanList']">plans</a> +      <span class="text-muted"> | </span> +      create plan +   </div> +</nav> + +<div class="page"> +   <span class="text-muted">For best results, use EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON)</span> +  <div> +    <input placeholder="name (optional)" class="input-box input-box-main" type="text" [(ngModel)]="newPlanName"> +    <button class="btn btn-primary btn-lg pull-right" (click)="submitPlan()">submit</button> +  </div> + +  <textarea placeholder="paste execution plan" class="input-box input-box-lg" [(ngModel)]="newPlanContent"></textarea> +  <textarea placeholder="paste corresponding SQL query" class="input-box input-box-lg" [(ngModel)]="newPlanQuery"></textarea> +</div> diff --git a/app/components/plan-new/plan-new.ts b/app/components/plan-new/plan-new.ts new file mode 100644 index 0000000..4a748ea --- /dev/null +++ b/app/components/plan-new/plan-new.ts @@ -0,0 +1,26 @@ +import {Component, OnInit} from 'angular2/core'; +import {Router, ROUTER_DIRECTIVES} from 'angular2/router'; +import {IPlan} from '../../interfaces/iplan'; + +import {PlanService} from '../../services/plan-service'; + +@Component({ +    selector: 'plan-new', +    templateUrl: './components/plan-new/plan-new.html', +    providers: [PlanService], +    directives: [ROUTER_DIRECTIVES] +}) +export class PlanNew { +    planIds: string[]; +    newPlanName: string; +    newPlanContent: string; +    newPlanQuery: string; +    newPlan: IPlan; + +    constructor( private _router: Router, private _planService: PlanService) { } + +    submitPlan() { +        this.newPlan = this._planService.createPlan(this.newPlanName, this.newPlanContent, this.newPlanQuery); +        this._router.navigate( ['PlanView', { id: this.newPlan.id }] ); +    } +} diff --git a/app/components/plan-node/plan-node.html b/app/components/plan-node/plan-node.html new file mode 100644 index 0000000..49380a2 --- /dev/null +++ b/app/components/plan-node/plan-node.html @@ -0,0 +1,50 @@ +<div class="plan-node"> +   <header (click)="showDetails = !showDetails"> +      <h4>{{node['Node Type'] | uppercase}}</h4> +      <span class="node-duration">{{duration}}<span class="text-muted">{{durationUnit}} | </span> +      <strong>{{executionTimePercent}}</strong><span class="text-muted">%</span> +      </span> +   </header> + +   <div class="relation-name" *ngIf="node['Relation Name']"><span class="text-muted">on </span> +      <span *ngIf="node['Schema']">{{node['Schema']}}.</span>{{node['Relation Name']}} +      <span *ngIf="node['Alias']"> ({{node['Alias']}})</span> +   </div> + +   <div class="relation-name" *ngIf="node['Group Key']"><span class="text-muted">by</span> {{node['Group Key']}}</div> +   <div class="relation-name" *ngIf="node['Sort Key']"><span class="text-muted">by</span> {{node['Sort Key']}}</div> +   <div class="relation-name" *ngIf="node['Join Type']">{{node['Join Type']}} <span class="text-muted">join</span></div> +   <div class="relation-name" *ngIf="node['Index Name']"><span class="text-muted">using</span> {{node['Index Name']}}</div> + +   <div class="tags" *ngIf="viewOptions.showTags && tags.length > 0"> +      <span *ngFor="#tag of tags">{{tag}}</span> +   </div> + +   <div *ngIf="currentHighlightType !== highlightTypes.NONE"> +      <div class="node-bar-container" [style.width]="(MAX_WIDTH+3)+'px'"> +         <span class="node-bar" [style.width]="width+'px'" [style.backgroundColor]="backgroundColor"></span> +      </div> +      <span class="node-bar-label"> +         <span class="text-muted">{{viewOptions.highlightType}}:</span> {{highlightValue | number:'.0-2'}} +      </span> +   </div> + +   <div class="planner-estimate" *ngIf="viewOptions.showPlannerEstimate"> +      <span *ngIf="plannerRowEstimateDirection === estimateDirections.over"><strong>over</strong> estimated rows</span> +      <span *ngIf="plannerRowEstimateDirection === estimateDirections.under"><strong>under</strong> estimated rows</span> +      <span> by <strong>{{plannerRowEstimateValue}}</strong>x</span> +   </div> + +   <table  *ngIf="showDetails" class="table prop-list"> +      <tr *ngFor="#prop of props"> +         <td width="40%">{{prop.key}}</td> +         <td>{{prop.value}}</td> +      <tr> +   </table> +</div> + +<ul *ngIf="node.Plans"> +  <li *ngFor="#subNode of node.Plans"> +    <plan-node [node]="subNode" [viewOptions]="viewOptions" [planStats]="planStats"></plan-node> +  </li> +</ul> diff --git a/app/components/plan-node/plan-node.ts b/app/components/plan-node/plan-node.ts new file mode 100644 index 0000000..3562ea6 --- /dev/null +++ b/app/components/plan-node/plan-node.ts @@ -0,0 +1,176 @@ +import {Component, OnInit} from 'angular2/core'; +import {HighlightType, EstimateDirection} from '../../enums'; +import {PlanService} from '../../services/plan-service'; +/// <reference path="lodash.d.ts" /> + +@Component({ +    selector: 'plan-node', +    inputs: ['node', 'planStats', 'viewOptions'], +    templateUrl: './components/plan-node/plan-node.html', +    directives: [PlanNode], +    providers: [PlanService] +}) + +export class PlanNode { +    // consts +    MAX_WIDTH: number = 220; +    MIN_ESTIMATE_MISS: number = 100; +    COSTLY_TAG: string = 'costliest'; +    SLOW_TAG: string = 'slowest'; +    LARGE_TAG: string = 'largest'; +    ESTIMATE_TAG: string = 'bad estimate'; + +    // inputs +    node: any; +    planStats: any; +    viewOptions: any; + +    // calculated properties +    duration: string; +    durationUnit: string; +    executionTimePercent: number; +    backgroundColor: string; +    highlightValue: number; +    width: number; +    props: Array<any>; +    tags: Array<string>; +    plannerRowEstimateValue: number; +    plannerRowEstimateDirection: EstimateDirection; +    currentHighlightType: string; + +    // expose enum to view +    estimateDirections = EstimateDirection; +    highlightTypes = HighlightType; + +    constructor(private _planService: PlanService) { } + +    ngOnInit() { +        this.currentHighlightType = this.viewOptions.highlightType; +        this.calculateBar(); +        this.calculateProps(); +        this.calculateDuration(); +        this.calculateTags(); + +        this.plannerRowEstimateDirection = this.node[this._planService.PLANNER_ESIMATE_DIRECTION]; +        this.plannerRowEstimateValue = _.round(this.node[this._planService.PLANNER_ESTIMATE_FACTOR]); +    } + +    ngDoCheck() { +        //   console.log("check", this.currentHighlightType, this.viewOptions.highlightType); +        if (this.currentHighlightType !== this.viewOptions.highlightType) { +            this.currentHighlightType = this.viewOptions.highlightType; +            this.calculateBar(); +        } +    } + +    calculateBar() { +        switch (this.currentHighlightType) { +            case HighlightType.DURATION: +                this.highlightValue = (this.node[this._planService.ACTUAL_DURATION_PROP]); +                this.width = Math.round((this.highlightValue / this.planStats.maxDuration) * this.MAX_WIDTH); +                break; +            case HighlightType.ROWS: +                this.highlightValue = (this.node[this._planService.ACTUAL_ROWS_PROP]); +                this.width = Math.round((this.highlightValue / this.planStats.maxRows) * this.MAX_WIDTH); +                break; +            case HighlightType.COST: +                this.highlightValue = (this.node[this._planService.ACTUAL_COST_PROP]); +                this.width = Math.round((this.highlightValue / this.planStats.maxCost) * this.MAX_WIDTH); +                break; +        } + +        if (this.width < 1) { this.width = 1 } +        this.backgroundColor = this.numberToColorHsl(1 - this.width / this.MAX_WIDTH); +    } + +    calculateDuration() { +        var dur: number = _.round(this.node[this._planService.ACTUAL_DURATION_PROP]); +        // convert duration into approriate units +        if (dur < 1) { +            this.duration = "<1"; +            this.durationUnit = 'ms'; +        } else if (dur > 1 && dur < 1000) { +            this.duration = dur.toString(); +            this.durationUnit = 'ms'; +        } else { +            this.duration = _.round(dur / 1000, 2).toString(); +            this.durationUnit = 'mins'; +        } +        this.executionTimePercent = (_.round((dur / this.planStats.executionTime) * 100)); +    } + +    // create an array of node propeties so that they can be displayed in the view +    calculateProps() { +        this.props = _.chain(this.node) +            .omit('Plans') +            .map((value, key) => { +                return { key: key, value: value }; +            }) +            .value(); +    } + +    calculateTags() { +        this.tags = []; +        if (this.node[this._planService.SLOWEST_NODE_PROP]) { +            this.tags.push(this.SLOW_TAG); +        } +        if (this.node[this._planService.COSTLIEST_NODE_PROP]) { +            this.tags.push(this.COSTLY_TAG); +        } +        if (this.node[this._planService.LARGEST_NODE_PROP]) { +            this.tags.push(this.LARGE_TAG); +        } +        if (this.node[this._planService.PLANNER_ESTIMATE_FACTOR] >= this.MIN_ESTIMATE_MISS) { +            this.tags.push(this.ESTIMATE_TAG); +        } +    } + +    /** +     * http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion +     * +     * Converts an HSL color value to RGB. Conversion formula +     * adapted from http://en.wikipedia.org/wiki/HSL_color_space. +     * Assumes h, s, and l are contained in the set [0, 1] and +     * returns r, g, and b in the set [0, 255]. +     * +     * @param   Number  h       The hue +     * @param   Number  s       The saturation +     * @param   Number  l       The lightness +     * @return  Array           The RGB representation +     */ +    hslToRgb(h, s, l) { +        var r, g, b; + +        if (s == 0) { +            r = g = b = l; // achromatic +        } else { +            function hue2rgb(p, q, t) { +                if (t < 0) t += 1; +                if (t > 1) t -= 1; +                if (t < 1 / 6) return p + (q - p) * 6 * t; +                if (t < 1 / 2) return q; +                if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; +                return p; +            } + +            var q = l < 0.5 ? l * (1 + s) : l + s - l * s; +            var p = 2 * l - q; +            r = hue2rgb(p, q, h + 1 / 3); +            g = hue2rgb(p, q, h); +            b = hue2rgb(p, q, h - 1 / 3); +        } + +        return [Math.floor(r * 255), Math.floor(g * 255), Math.floor(b * 255)]; +    } + +    // convert a number to a color using hsl +    numberToColorHsl(i) { +        // as the function expects a value between 0 and 1, and red = 0° and green = 120° +        // we convert the input to the appropriate hue value +        var hue = i * 100 * 1.2 / 360; +        // we convert hsl to rgb (saturation 100%, lightness 50%) +        var rgb = this.hslToRgb(hue, .9, .4); +        // we format to css value and return +        return 'rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ')'; +    } +} diff --git a/app/components/plan-view/plan-view.html b/app/components/plan-view/plan-view.html new file mode 100644 index 0000000..ab9b7dc --- /dev/null +++ b/app/components/plan-view/plan-view.html @@ -0,0 +1,72 @@ +<div class="menu" [class.menu-hidden]="hideMenu"> +   <header> +      <i class="fa fa-cogs menu-toggle" (click)="hideMenu = !hideMenu"></i> +      <h3>display options</h3> +   </header> + +   <ul> +      <li> +         <input id="showPlanStats" type="checkbox" [(ngModel)]="viewOptions.showPlanStats"> +         <label class="clickable" for="showPlanStats"> show plan stats</label> +      </li> + +      <li> +         <input id="showPlannerEstimate" type="checkbox" [(ngModel)]="viewOptions.showPlannerEstimate"> +         <label class="clickable" for="showPlannerEstimate"> show planner estimate</label> +      </li> + +      <li> +         <input id="showTags" type="checkbox" [(ngModel)]="viewOptions.showTags"> +         <label class="clickable" for="showTags"> show analysis tags</label> +      </li> + +      <li> +         <label>graph metric: </label> +         <select [(ngModel)]="viewOptions.highlightType"> +            <option value="{{highlightTypes.NONE}}">{{highlightTypes.NONE}}</option> +            <option value="{{highlightTypes.DURATION}}">{{highlightTypes.DURATION}}</option> +            <option value="{{highlightTypes.ROWS}}">{{highlightTypes.ROWS}}</option> +            <option value="{{highlightTypes.COST}}">{{highlightTypes.COST}}</option> +         </select> +      </li> +   </ul> +</div> + +<nav> +   <div class="nav-container"> +      <a [routerLink]="['PlanList']">plans</a><span class="text-muted"> / </span>{{plan.name}} +   </div> +</nav> + +<div class="page page-stretch"> +   <div *ngIf="viewOptions.showPlanStats" class="plan-stats"> +      <div> +         <span class="stat-value">{{executionTime}}</span> +         <span class="stat-label">execution time ({{executionTimeUnit}})</span> +      </div> +      <div *ngIf="planStats.planningTime"> +         <span class="stat-value">{{planStats.planningTime | number:'.0-2'}}</span> +         <span class="stat-label">planning time (ms)</span> +      </div> +      <div *ngIf="planStats.maxDuration"> +         <span class="stat-value">{{planStats.maxDuration | number:'.0-2'}}</span> +         <span class="stat-label">slowest node (ms)</span> +      </div> +      <div *ngIf="planStats.maxRows"> +         <span class="stat-value">{{planStats.maxRows | number:'.0-2'}}</span> +         <span class="stat-label">largest node (rows)</span> +      </div> +      <div *ngIf="planStats.maxCost"> +         <span class="stat-value">{{planStats.maxCost | number:'.0-2'}}</span> +         <span class="stat-label">costliest node</span> +      </div> +   </div> + +   <div class="plan"> +      <ul> +         <li> +            <plan-node [node]="rootContainer.Plan" [planStats]="planStats" [viewOptions]="viewOptions"></plan-node> +         </li> +      </ul> +   </div> +</div> diff --git a/app/components/plan-view/plan-view.ts b/app/components/plan-view/plan-view.ts new file mode 100644 index 0000000..ab4e6a3 --- /dev/null +++ b/app/components/plan-view/plan-view.ts @@ -0,0 +1,96 @@ +import {Component, OnInit} from 'angular2/core'; +import {RouteParams} from 'angular2/router'; +import {ROUTER_DIRECTIVES} from 'angular2/router'; + +import {IPlan} from '../../interfaces/iplan'; +import {PlanService} from '../../services/plan-service'; +import {HighlightType} from '../../enums'; +import {PlanNode} from '../plan-node/plan-node'; + +@Component({ +    selector: 'plan-view', +    templateUrl: './components/plan-view/plan-view.html', +    directives: [ROUTER_DIRECTIVES, PlanNode], +    providers: [PlanService] +}) +export class PlanView { +    id: string; +    plan: IPlan; +    rootContainer: any; +    executionTime: string; +    executionTimeUnit: string; +    hideMenu: boolean = true; + +    planStats: any = { +        executionTime: 0, +        maxRows: 0, +        maxCost: 0, +        maxDuration: 0 +    }; + +    viewOptions: any = { +        showPlanStats: true, +        showHighlightBar: true, +        showPlannerEstimate: false, +        showTags: true, +        highlightType: HighlightType.NONE +    }; + +    showPlannerEstimate: boolean = true; +    showMenu: boolean = false; + +    highlightTypes = HighlightType; // exposing the enum to the view + +    constructor(private _planService: PlanService, routeParams: RouteParams) { +        this.id = routeParams.get('id'); +    } + +    getPlan() { +        if (!this.id) { +            return; +        } + +        this.plan = this._planService.getPlan(this.id); +        this.rootContainer = this.plan.content; + +        var executionTime: number = this.rootContainer['Execution Time'] || this.rootContainer['Total Runtime']; +        [this.executionTime, this.executionTimeUnit] = this.calculateDuration(executionTime); + +        this.planStats = { +            executionTime: executionTime, +            planningTime: this.rootContainer['Planning Time'], +            maxRows: this.rootContainer[this._planService.MAXIMUM_ROWS_PROP], +            maxCost: this.rootContainer[this._planService.MAXIMUM_COSTS_PROP], +            maxDuration: this.rootContainer[this._planService.MAXIMUM_DURATION_PROP] +        } +    } + +    ngOnInit() { +        this.getPlan(); +    } + +    toggleHighlight(type: HighlightType) { +        this.viewOptions.highlightType = type; +    } + +    analyzePlan() { +        this._planService.analyzePlan(this.plan); +    } + +    calculateDuration(originalValue: number) { +        var duration: string = ''; +        var unit: string = ''; + +        if (originalValue < 1) { +            duration = "<1"; +            unit = 'ms'; +        } else if (originalValue > 1 && originalValue < 1000) { +            duration = originalValue.toString(); +            unit = 'ms'; +        } else { +            duration = _.round(originalValue / 1000, 2).toString(); +            unit = 'mins'; +        } +        return [duration, unit]; +    } +} | 
