File

src/app/shared/node-viewer/node-viewer.component.ts

Implements

OnInit

Metadata

Index

Properties
Methods
Inputs
Outputs
Accessors

Constructor

Public constructor(dialog: MatDialog, route: ActivatedRoute, resourceService: ResourceService, helper: HelperService)
Parameters :
Name Type Optional
dialog MatDialog No
route ActivatedRoute No
resourceService ResourceService No
helper HelperService No

Inputs

loadingIndicator
Type : boolean
Default value : false
obj
Type : any
unlockable
Type : boolean
Default value : false

Outputs

create
Type : EventEmitter<Node>
delete
Type : EventEmitter<Node>
update
Type : EventEmitter<Node>

Methods

beforeDelete
beforeDelete(type, row)
Parameters :
Name Optional
type No
row No
Returns : void
created
created(type, data, key)
Parameters :
Name Optional
type No
data No
key No
Returns : void
edited
edited(type, undefined, key, isDeleting)
Parameters :
Name Optional
type No
No
key No
isDeleting No
Returns : void
getNameCellClass
getNameCellClass(undefined)
Parameters :
Name Optional
No
Returns : any
ngOnInit
ngOnInit()
Returns : void
onCreate
onCreate(type)
Parameters :
Name Optional
type No
Returns : void
onDelete
onDelete(type, row)
Parameters :
Name Optional
type No
row No
Returns : void
updateFilter
updateFilter(event)
Parameters :
Name Optional
event No
Returns : void

Properties

Private _editable
Default value : false
_listConfigs
Type : any[]
_mapConfigs
Type : any[]
Protected _obj
Type : any
_simpleConfigs
Type : any[]
clusterName
Type : string
columns
Type : object
Default value : { simpleConfigs: [ { name: 'Name', editable: false, }, { name: 'Value', editable: false, }, ], listConfigs: [ { name: 'Value', editable: false, }, ], }
headerHeight
Default value : Settings.tableHeaderHeight
isLoading
Default value : true
keyword
Type : string
Default value : ''
listTable
Decorators :
@ViewChild('listTable', {static: true})
mapTable
Decorators :
@ViewChild('mapTable', {static: true})
Protected node
Type : Node
resourceName
Type : string
rowHeight
Default value : Settings.tableRowHeight
simpleTable
Decorators :
@ViewChild('simpleTable', {static: true})
sorts
Type : []
Default value : [{ prop: 'name', dir: 'asc' }]

Accessors

obj
getobj()
setobj(value: any)
Parameters :
Name Type Optional
value any No
Returns : void
objString
getobjString()
setobjString(value: string | null)
Parameters :
Name Type Optional
value string | null No
Returns : void
editable
geteditable()
seteditable(value: boolean)
Parameters :
Name Type Optional
value boolean No
Returns : void
simpleConfigs
getsimpleConfigs()
listConfigs
getlistConfigs()
mapConfigs
getmapConfigs()
import {
  Component,
  OnInit,
  Input,
  Output,
  ViewChild,
  ViewEncapsulation,
  EventEmitter,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';

import * as _ from 'lodash';
// import * as ace from 'ace-builds/src-noconflict/ace';
import 'ace-builds';
import 'ace-builds/webpack-resolver';
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/theme-chrome';
import { config } from 'ace-builds';

import { ResourceService } from '../../resource/shared/resource.service';
import { HelperService } from '../../shared/helper.service';
import { Node } from '../models/node.model';
import { Settings } from '../../core/settings';
import { InputDialogComponent } from '../dialog/input-dialog/input-dialog.component';
import { ConfirmDialogComponent } from '../dialog/confirm-dialog/confirm-dialog.component';

export type IdealState = {
  id: string;
  simpleFields?: { [key: string]: any };
  listFields?: { [key: string]: any };
  mapFields?: { [key: string]: any };
};

config.set(
  'basePath',
  'https://cdn.jsdelivr.net/npm/ace-builds@1.6.0/src-noconflict/'
);
config.setModuleUrl(
  'ace/mode/javascript_worker',
  'https://cdn.jsdelivr.net/npm/ace-builds@1.6.0/src-noconflict/worker-json.js'
);
@Component({
  selector: 'hi-node-viewer',
  templateUrl: './node-viewer.component.html',
  styleUrls: ['./node-viewer.component.scss'],
  providers: [ResourceService],
  // Since we are importing external styles in this component
  // we will not use Shadow DOM at all to make sure the styles apply
  encapsulation: ViewEncapsulation.None,
})
export class NodeViewerComponent implements OnInit {
  isLoading = true;
  clusterName: string;
  resourceName: string;
  @ViewChild('simpleTable', { static: true }) simpleTable;
  @ViewChild('listTable', { static: true }) listTable;
  @ViewChild('mapTable', { static: true }) mapTable;

  @Output('update')
  change: EventEmitter<Node> = new EventEmitter<Node>();

  @Output('create')
  create: EventEmitter<Node> = new EventEmitter<Node>();

  @Output('delete')
  delete: EventEmitter<Node> = new EventEmitter<Node>();

  @Input()
  unlockable = false;

  @Input()
  loadingIndicator = false;

  private _editable = false;

  protected _obj: any;
  protected node: Node;

  headerHeight = Settings.tableHeaderHeight;
  rowHeight = Settings.tableRowHeight;
  sorts = [{ prop: 'name', dir: 'asc' }];
  keyword = '';
  columns = {
    simpleConfigs: [
      {
        name: 'Name',
        editable: false,
      },
      {
        name: 'Value',
        editable: false,
      },
    ],
    listConfigs: [
      {
        name: 'Value',
        editable: false,
      },
    ],
  };

  _simpleConfigs: any[];

  _listConfigs: any[];

  _mapConfigs: any[];

  // MODE 1: use directly in components
  @Input()
  set obj(value: any) {
    if (value !== null) {
      this._obj = value;
      this.node = new Node(value);
    }
  }
  get obj(): any {
    return this._obj;
  }
  set objString(value: string | null) {
    let parsedValue = null;
    if (value && value !== null) {
      parsedValue = JSON.parse(value);
    }

    this.obj = parsedValue;
    this.node = new Node(parsedValue);
  }
  get objString(): string {
    return JSON.stringify(this.obj, null, 2);
  }
  set editable(value: boolean) {
    this._editable = value;
    this.columns.simpleConfigs[1].editable = this._editable;
    this.columns.listConfigs[0].editable = this._editable;
  }
  get editable() {
    return this._editable;
  }
  get simpleConfigs(): any[] {
    return this.node
      ? _.filter(
          this.node.simpleFields,
          (config) =>
            config.name.toLowerCase().indexOf(this.keyword) >= 0 ||
            config.value.toLowerCase().indexOf(this.keyword) >= 0
        )
      : [];
  }
  get listConfigs(): any[] {
    return this.node
      ? _.filter(
          this.node.listFields,
          (config) =>
            config.name.toLowerCase().indexOf(this.keyword) >= 0 ||
            _.some(
              config.value as any[],
              (subconfig) =>
                subconfig.value.toLowerCase().indexOf(this.keyword) >= 0
            )
        )
      : [];
  }
  get mapConfigs(): any[] {
    return this.node
      ? _.filter(
          this.node.mapFields,
          (config) =>
            config.name.toLowerCase().trim().indexOf(this.keyword) >= 0 ||
            _.some(
              config.value as any[],
              (subconfig) =>
                subconfig.name.toLowerCase().trim().indexOf(this.keyword) >=
                  0 || subconfig.value.toLowerCase().indexOf(this.keyword) >= 0
            )
        )
      : [];
  }

  public constructor(
    protected dialog: MatDialog,
    protected route: ActivatedRoute,
    protected resourceService: ResourceService,
    protected helper: HelperService
  ) {}

  ngOnInit() {
    // MODE 2: use in router
    if (this.route.snapshot.data.path) {
      const path = this.route.snapshot.data.path;

      // try parent data first
      this.obj = _.get(this.route.parent, `snapshot.data.${path}`);

      if (this.obj == null) {
        // try self data then
        this.obj = _.get(this.route.snapshot.data, path);
      }

      this.objString = JSON.stringify(this.obj, null, 2);
    }

    if (this.route.parent) {
      this.clusterName =
        this.route.parent.snapshot.params.name ||
        this.route.parent.snapshot.params.cluster_name;
      this.resourceName = this.route.parent.snapshot.params.resource_name;
    }
  }

  updateFilter(event) {
    this.keyword = event.target.value.toLowerCase().trim();

    // Whenever the filter changes, always go back to the first page
    if (this.simpleTable) {
      this.simpleTable.offset = 0;
    }
    if (this.listTable) {
      this.listTable.offset = 0;
    }
    if (this.mapTable) {
      this.mapTable.offset = 0;
    }
  }

  getNameCellClass({ value }): any {
    return {
      // highlight HELIX own configs
      primary: _.snakeCase(value).toUpperCase() === value,
    };
  }

  onCreate(type) {
    this.dialog
      .open(InputDialogComponent, {
        data: {
          title: `Create a new ${type} configuration`,
          message:
            "Please enter the name of the new configuration. You'll be able to add values later:",
          values: {
            name: {
              label: 'the name of the new configuration',
            },
          },
        },
      })
      .afterClosed()
      .subscribe((result) => {
        if (result) {
          const entry = [
            {
              name: result.name.value,
              value: [],
            },
          ];

          const newNode: Node = new Node(null);
          if (type === 'list') {
            newNode.listFields = entry;
          } else if (type === 'map') {
            newNode.mapFields = entry;
          }

          this.create.emit(newNode);
        }
      });
  }

  beforeDelete(type, row) {
    this.dialog
      .open(ConfirmDialogComponent, {
        data: {
          title: 'Confirmation',
          message: 'Are you sure you want to delete this configuration?',
        },
      })
      .afterClosed()
      .subscribe((result) => {
        if (result) {
          this.onDelete(type, row);
        }
      });
  }

  onDelete(type, row) {
    const newNode: Node = new Node(null);

    if (type === 'simple') {
      newNode.appendSimpleField(row.name, '');
    } else if (type === 'list') {
      newNode.listFields = [{ name: row.name.trim(), value: [] }];
    } else if (type === 'map') {
      newNode.mapFields = [{ name: row.name.trim(), value: null }];
    }

    this.delete.emit(newNode);
  }

  created(type, data, key) {
    const newNode: Node = new Node(null);

    switch (type) {
      case 'simple':
        newNode.appendSimpleField(data.name.value, data.value.value);
        break;

      case 'list':
        if (key) {
          const entry = _.find(this.node.listFields, { name: key });

          entry.value.push({
            name: '',
            value: data.value.value,
          });

          newNode.listFields.push(entry);
        }
        break;

      case 'map':
        if (key) {
          const entry = _.find(this.node.mapFields, { name: key.trim() });

          _.forEach(entry.value, (item: any) => {
            newNode.appendMapField(key, item.name, item.value);
          });

          newNode.appendMapField(key, data.name.value, data.value.value);
        }
        break;
    }

    this.create.emit(newNode);
  }

  edited(type, { row, column, value }, key, isDeleting) {
    if (!isDeleting && column.name !== 'Value') {
      return;
    }

    const newNode: Node = new Node(null);

    switch (type) {
      case 'simple':
        newNode.appendSimpleField(row.name, value);
        break;

      case 'list':
        if (key) {
          const entry = _.find(this.node.listFields, { name: key });
          const index = _.findIndex(entry.value, { value: row.value });

          if (isDeleting) {
            entry.value.splice(index, 1);
          } else {
            entry.value[index].value = value;
          }

          newNode.listFields.push(entry);
        }
        break;

      case 'map':
        if (key) {
          // have to fetch all other configs under this key
          const entry = _.find(this.node.mapFields, { name: key.trim() });
          newNode.mapFields = [{ name: key.trim(), value: [] }];

          _.forEach(entry.value, (item: any) => {
            if (item.name === row.name) {
              if (!isDeleting) {
                newNode.appendMapField(key.trim(), item.name.trim(), value);
              }
            } else {
              newNode.appendMapField(key.trim(), item.name.trim(), item.value);
            }
          });
        }
        break;
    }

    const path = this?.route?.snapshot?.data?.path;
    if (path && path === 'idealState') {
      const idealState: IdealState = {
        id: this.resourceName,
      };

      // format the payload the way that helix-rest expects
      // before: { simpleFields: [{ name: 'NUM_PARTITIONS', value: 2 }] };
      // after:  { simpleFields: { NUM_PARTITIONS: 2 } };
      function appendIdealStateProperty(property: keyof Node) {
        if (Array.isArray(newNode[property]) && newNode[property].length > 0) {
          idealState[property] = {} as any;
          (newNode[property] as any[]).forEach((field) => {
            idealState[property][field.name] = field.value;
          });
        }
      }
      Object.keys(newNode).forEach((key) =>
        appendIdealStateProperty(key as keyof Node)
      );

      const observer = this.resourceService.setIdealState(
        this.clusterName,
        this.resourceName,
        idealState
      );

      if (observer) {
        this.isLoading = true;
        observer.subscribe(
          () => {
            this.helper.showSnackBar('Ideal State updated!');
          },
          (error) => {
            this.helper.showError(error);
            this.isLoading = false;
          },
          () => (this.isLoading = false)
        );
      }
    }

    this.change.emit(newNode);
  }
}
<!--
  ~ 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.
  -->

<section
  class="node-viewer"
  fxLayout="column"
  fxLayoutAlign="center center"
  fxLayoutGap="10px"
>
  <mat-progress-bar
    *ngIf="loadingIndicator"
    mode="indeterminate"
  ></mat-progress-bar>
  <mat-button-toggle-group #group="matButtonToggleGroup" value="table">
    <mat-button-toggle value="table"> Table View </mat-button-toggle>
    <mat-button-toggle value="tree"> Tree View </mat-button-toggle>
    <mat-button-toggle value="json"> JSON View </mat-button-toggle>
  </mat-button-toggle-group>
  <section class="viewer" [ngSwitch]="group.value" fxFlexFill>
    <ngx-json-viewer *ngSwitchCase="'tree'" [json]="obj"></ngx-json-viewer>
    <ace-editor
      *ngSwitchCase="'json'"
      [(text)]="objString"
      mode="json"
      theme="chrome"
      [options]="{ useWorker: false }"
      style="min-height: 300px"
      #editor
    >
    </ace-editor>
    <section *ngSwitchCase="'table'">
      <!-- TODO vxu: use mat-simple-table when it's available -->

      <section fxLayout="row" fxLayoutAlign="center center">
        <span fxFlex="1 1 auto"></span>
        <mat-icon>search</mat-icon>
        <mat-form-field class="search-form-field">
          <input
            matInput
            placeholder="Type to filter the fields..."
            (keyup)="updateFilter($event)"
          />
        </mat-form-field>
        <span fxFlex="1 1 auto"></span>
        <!-- *ngIf="unlockable" -->
        <button
          mat-button
          (click)="editable = !editable"
          [matTooltip]="
            editable
              ? 'Click to prevent further changes'
              : 'Click to make changes'
          "
        >
          <mat-icon>{{ editable ? 'lock_open' : 'lock' }}</mat-icon>
          {{ editable ? 'Unlocked' : 'Locked' }}
        </button>
      </section>

      <mat-card>
        <mat-card-header>
          <mat-card-title>
            Simple Fields
            <span *ngIf="simpleConfigs.length == 0">is empty.</span>
            <span *ngIf="keyword" class="primary">(filtered)</span>
          </mat-card-title>
        </mat-card-header>
        <mat-card-content>
          <hi-data-table
            *ngIf="simpleConfigs.length || editable"
            [rows]="simpleConfigs"
            [sorts]="sorts"
            [columns]="columns.simpleConfigs"
            [deletable]="editable"
            [insertable]="editable"
            (update)="edited('simple', $event)"
            (create)="created('simple', $event)"
            (delete)="onDelete('simple', $event.row)"
          >
          </hi-data-table>
        </mat-card-content>
      </mat-card>

      <mat-card>
        <mat-card-header>
          <mat-card-title>
            List Fields
            <span *ngIf="listConfigs.length == 0">is empty.</span>
            <span *ngIf="keyword" class="primary">(filtered)</span>
          </mat-card-title>
        </mat-card-header>
        <mat-card-content>
          <ngx-datatable
            *ngIf="listConfigs.length || editable"
            #listTable
            class="material"
            [headerHeight]="headerHeight"
            rowHeight="auto"
            [footerHeight]="headerHeight"
            columnMode="force"
            [rows]="listConfigs"
            [sorts]="sorts"
            [limit]="10"
          >
            <ngx-datatable-column
              *ngIf="editable"
              [width]="40"
              [resizeable]="false"
              [draggable]="false"
              [canAutoResize]="false"
            >
              <ng-template let-row="row" ngx-datatable-cell-template>
                <button
                  mat-icon-button
                  class="delete-button"
                  matTooltip="Click to delete"
                  (click)="beforeDelete('list', row)"
                >
                  <mat-icon>delete_forever</mat-icon>
                </button>
              </ng-template>
            </ngx-datatable-column>
            <ngx-datatable-column
              name="Name"
              [width]="80"
              [cellClass]="getNameCellClass"
            ></ngx-datatable-column>
            <ngx-datatable-column name="Value" [width]="400">
              <ng-template
                let-row="row"
                let-value="value"
                ngx-datatable-cell-template
              >
                <hi-data-table
                  [rows]="value"
                  [sorts]="sorts"
                  [columns]="columns.listConfigs"
                  [deletable]="editable"
                  [insertable]="editable"
                  (update)="edited('list', $event, row.name)"
                  (create)="created('list', $event, row.name)"
                  (delete)="edited('list', $event, row.name, true)"
                >
                </hi-data-table>
              </ng-template>
            </ngx-datatable-column>
            <ngx-datatable-footer>
              <ng-template
                ngx-datatable-footer-template
                let-rowCount="rowCount"
                let-pageSize="pageSize"
                let-curPage="curPage"
              >
                <section
                  class="footer"
                  fxLayout="row"
                  fxLayoutAlign="space-between center"
                >
                  <button
                    mat-button
                    *ngIf="editable"
                    (click)="onCreate('list')"
                  >
                    <mat-icon>add</mat-icon>
                    Add new entry
                  </button>
                  <section>{{ rowCount }} total</section>
                  <section>
                    <datatable-pager
                      [pagerLeftArrowIcon]="'datatable-icon-left'"
                      [pagerRightArrowIcon]="'datatable-icon-right'"
                      [pagerPreviousIcon]="'datatable-icon-prev'"
                      [pagerNextIcon]="'datatable-icon-skip'"
                      [page]="curPage"
                      [size]="pageSize"
                      [count]="rowCount"
                      [hidden]="!(rowCount / pageSize > 1)"
                      (change)="listTable.onFooterPage($event)"
                    >
                    </datatable-pager>
                  </section>
                </section>
              </ng-template>
            </ngx-datatable-footer>
          </ngx-datatable>
        </mat-card-content>
      </mat-card>

      <mat-card>
        <mat-card-header>
          <mat-card-title>
            Map Fields
            <span *ngIf="mapConfigs.length == 0">is empty.</span>
            <span *ngIf="keyword" class="primary">(filtered)</span>
          </mat-card-title>
        </mat-card-header>
        <mat-card-content>
          <ngx-datatable
            *ngIf="mapConfigs.length || editable"
            #mapTable
            class="material"
            [headerHeight]="headerHeight"
            rowHeight="auto"
            [footerHeight]="headerHeight"
            columnMode="force"
            [rows]="mapConfigs"
            [sorts]="sorts"
            [limit]="10"
          >
            <ngx-datatable-column
              *ngIf="editable"
              [width]="40"
              [resizeable]="false"
              [draggable]="false"
              [canAutoResize]="false"
            >
              <ng-template let-row="row" ngx-datatable-cell-template>
                <button
                  mat-icon-button
                  class="delete-button"
                  matTooltip="Click to delete"
                  (click)="beforeDelete('map', row)"
                >
                  <mat-icon>delete_forever</mat-icon>
                </button>
              </ng-template>
            </ngx-datatable-column>
            <ngx-datatable-column
              name="Name"
              [width]="80"
              [cellClass]="getNameCellClass"
            ></ngx-datatable-column>
            <ngx-datatable-column name="Value" [width]="500">
              <ng-template
                let-row="row"
                let-value="value"
                ngx-datatable-cell-template
              >
                <hi-data-table
                  [rows]="value"
                  [sorts]="sorts"
                  [columns]="columns.simpleConfigs"
                  [deletable]="editable"
                  [insertable]="editable"
                  (update)="edited('map', $event, row.name)"
                  (create)="created('map', $event, row.name)"
                  (delete)="edited('map', $event, row.name, true)"
                >
                </hi-data-table>
              </ng-template>
            </ngx-datatable-column>
            <ngx-datatable-footer>
              <ng-template
                ngx-datatable-footer-template
                let-rowCount="rowCount"
                let-pageSize="pageSize"
                let-curPage="curPage"
              >
                <section
                  class="footer"
                  fxLayout="row"
                  fxLayoutAlign="space-between center"
                >
                  <button mat-button *ngIf="editable" (click)="onCreate('map')">
                    <mat-icon>add</mat-icon>
                    Add new entry
                  </button>
                  <section>{{ rowCount }} total</section>
                  <section>
                    <datatable-pager
                      [pagerLeftArrowIcon]="'datatable-icon-left'"
                      [pagerRightArrowIcon]="'datatable-icon-right'"
                      [pagerPreviousIcon]="'datatable-icon-prev'"
                      [pagerNextIcon]="'datatable-icon-skip'"
                      [page]="curPage"
                      [size]="pageSize"
                      [count]="rowCount"
                      [hidden]="!(rowCount / pageSize > 1)"
                      (change)="mapTable.onFooterPage($event)"
                    >
                    </datatable-pager>
                  </section>
                </section>
              </ng-template>
            </ngx-datatable-footer>
          </ngx-datatable>
        </mat-card-content>
      </mat-card>
    </section>
  </section>
</section>

./node-viewer.component.scss

@use '@angular/material' as mat;
@import 'src/theme.scss';

.node-viewer {
  padding: 10px;
}

.primary {
  color: mat.get-color-from-palette($hi-primary);
}

.search-form-field {
  width: 300px;
  padding: 10px 0 0 5px;
}

mat-card {
  margin-bottom: 10px;
}

ngx-datatable {
  word-break: break-all;
}

.footer {
  width: 100%;
  padding: 0 20px;
}

.delete-button {
  width: 24px;
  height: 24px;
  line-height: 24px;

  .mat-icon {
    @include md-icon-size(20px);

    &:hover {
      color: mat.get-color-from-palette($hi-warn);
    }
  }
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""