ui-modules/app-inspector/app/components/config-sensor-table/config-sensor-table.directive.js (130 lines of code) (raw):

/* * 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 angular from "angular"; import ngSanitize from "angular-sanitize"; import ngClipboard from "ngclipboard"; import template from "./config-sensor-table.template.html"; import jsyaml from 'js-yaml'; import { isSensitiveFieldName } from "brooklyn-ui-utils/sensitive-field/sensitive-field"; const MODULE_NAME = 'inspector.config-sensor.table'; angular.module(MODULE_NAME, [ngSanitize, ngClipboard]) .directive('configSensorTable', ['brSnackbar', configSensorTableDirective]) .filter('brLinky', ['$filter', '$state', '$sanitize', brLinkyFilter]); export default MODULE_NAME; export function configSensorTableDirective(brSnackbar) { return { restrict: 'E', template: template, scope: { data: '=', info: '=', checkPlaintextSensitiveKeyValue: '<', reconfigureCallback: '<', }, link, }; function link(scope) { scope.items = []; scope.itemsCache = {}; scope.WARNING_TEXT = 'This value is identified as potentially sensitive based and so is masked here. ' + 'The value should be supplied as a DSL expression not as plain text. ' + 'Note that the unmasked value still might not reveal the actual value, ' + 'if sensitive values are blocked by the API or if DSL resolution is skipped.'; function dumpAndTruncate(x) { let result = jsyaml.dump(x); if (result && result.length > 100000) { result = result.slice(0, 100000) + '\n...'; } return result; } function update() { if (angular.isObject(scope.data) && scope.mapInfo) { const dataPlusReconfigurable = Object.assign({}, scope.data); scope.info.forEach(info => { if (info.reconfigurable && !dataPlusReconfigurable.hasOwnProperty(info.name)) { dataPlusReconfigurable[info.name] = undefined; } }); scope.items = Object.entries(dataPlusReconfigurable) .map(([key, value]) => { const old = scope.itemsCache[key]; if (old && _.isEqual(value, old.value)) { return old; } const isObject = angular.isObject(value); scope.mapInfo[key] = Object.assign({isObject}, scope.mapInfo[key]); // minimize calls to yamlification; can take 100ms+ for large objects // (and no nice way i can see to reduce that or to background it) let valueDumped = isObject ? _.escape(dumpAndTruncate(value)) : value; const result = { key, value, valueDumped, isPlaintextSensitiveValue: scope.checkPlaintextSensitiveKeyValue && scope.checkPlaintextSensitiveKeyValue(key, value), }; scope.itemsCache[key] = result; return result; }); } } scope.$watchGroup(['data'], (changes)=> { update(); }); scope.$watch('info', () => { if (angular.isArray(scope.info)) { if (!scope.mapInfo) scope.mapInfo = {}; scope.mapInfo = scope.info.reduce((pool, infoItem) => { pool[infoItem.name] = Object.assign({}, scope.mapInfo[infoItem.name], infoItem); return pool; }, {}); update(); } }); scope.onClipboardSuccess = (e)=> { angular.element(e.trigger).triggerHandler('copied'); e.clearSelection(); }; scope.onClipboardError = (e)=> { let message = ''; let actionKey = e.action === 'cut' ? 'X' : 'C'; if(/iPhone|iPad/i.test(navigator.userAgent)) { message = 'No support :('; } else if(/Mac/i.test(navigator.userAgent)) { message = 'Press ⌘-' + actionKey + ' to ' + e.action; } else { message = 'Press Ctrl-' + actionKey + ' to ' + e.action; } brSnackbar.create(message); }; scope.isNullish = (x) => x===null || typeof x === 'undefined'; } } function asJsonIfJson(input, isKnownString, isYamledObject, $sanitize) { if (isKnownString) { if (!input) return null; let inputTrimmed = input.trim(); if ((inputTrimmed.startsWith("{") && inputTrimmed.endsWith("}")) || (inputTrimmed.startsWith("[") && inputTrimmed.endsWith("]"))) { // looks like json try { input = JSON.parse(inputTrimmed); } catch (okayIfNotJson) { return null; } } else { return null; } } const yamld = isYamledObject ? input : _.escape(jsyaml.dump(input)); return $sanitize('<div class="multiline-code">' + yamld + '</div>'); } export function brLinkyFilter($filter, $state, $sanitize) { // render links as html, and everything else as not html. return function(input, key, target, attributes) { if (input == null) { return ''; } else if (angular.isObject(key) && key.isObject) { return asJsonIfJson(input, false, true, $sanitize) || $filter('linky')(angular.toJson(input), target, attributes); } else if (!angular.isString(input)) { return asJsonIfJson(input, false, false, $sanitize) || $filter('linky')(angular.toJson(input), target, attributes); } else if (angular.isObject(key) && angular.isString(key.name) && (key.name.indexOf('ssh') > -1 || isSensitiveFieldName(key.name))) { return input; } else if (angular.isObject(key) && key.links && key.links.hasOwnProperty('action:open')) { let matches = key.links['action:open'].match(/\#\/v1\/applications\/([^\/]+)\/entities\/([^\/]+)/i); return matches !== null ? $sanitize('<a href="' + $state.href('main.inspect.summary', {applicationId: matches[1], entityId: matches[2]}) + '">' + input + '</a>') : $filter('linky')(input, target, attributes); } else { return asJsonIfJson(input, true, false, $sanitize) || $filter('linky')(input, target, attributes); } } }