apollo-enhance

make old apollo better

// ==UserScript==
// @name         apollo-enhance
// @namespace    apollo-enhance
// @version      0.9.16
// @description  make old apollo better
// @homepage     https://github.com/xyz327/old-apollo-portal-enhance
// @website      https://github.com/xyz327/old-apollo-portal-enhance
// @source       https://github.com/xyz327/old-apollo-portal-enhance
// @author       xizhou
// @match        *://*/config.html*
// @resource     highlight_xcode_css https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/highlight.js/9.18.5/styles/xcode.min.css
// @require      https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/diff_match_patch/20121119/diff_match_patch_uncompressed.js
// @require      https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/highlight.js/9.18.5/highlight.min.js
// @require      https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/highlight.js/9.18.5/languages/json.min.js
// @resource     text_different_css https://cdn.jsdelivr.net/npm/[email protected]/build/style/text-different.min.css
// @require      https://cdn.jsdelivr.net/combine/npm/[email protected]/build/text-different.min.js,npm/[email protected]/build/text-different-for-html.min.js
// @noframes
// @grant      GM_getResourceText
// @grant      GM_addStyle
// @grant      GM_addElement
// ==/UserScript==

(function (_$1) {
  'use strict';

  var allFeature = [
  	{
  		name: "fixEnvTab",
  		desc: "固定左侧菜单",
  		defaultEnabled: true
  	},
  	{
  		name: "fixNiceScroll",
  		desc: "修复 CTRL+F 搜索不能跳转的问题",
  		defaultEnabled: true
  	},
  	{
  		name: "gotoNamespace",
  		desc: "一键跳转到对应的 namespace",
  		defaultEnabled: true
  	},
  	{
  		name: "releaseDiff",
  		desc: "发布界面差异对比",
  		defaultEnabled: true
  	},
  	{
  		name: "releaseModal",
  		desc: "发布界面增强",
  		defaultEnabled: true
  	},
  	{
  		name: "showText",
  		desc: "单 key 差异对比",
  		defaultEnabled: true
  	},
  	{
  		name: "stash",
  		more: true,
  		desc: "",
  		defaultEnabled: false,
  		enabledWarn: "实验性功能,请谨慎操作"
  	},
  	{
  		name: "copyNamespace",
  		desc: "复制namespace",
  		defaultEnabled: true
  	},
  	{
  		name: "prodWarnDisable",
  		desc: "谨慎使用",
  		more: true,
  		defaultEnabled: false,
  		enabledWarn: "实验性功能,请谨慎操作"
  	},
  	{
  		name: "valueCodeEditor",
  		desc: "配置值编辑器增强",
  		defaultEnabled: false
  	}
  ];

  var enhanceNavId = "apollo-enhance-nav";
  var featureId = "apollo-enhance-feature";
  var allFeatureMap = {};
  allFeature.forEach((feature) => {
    allFeatureMap[feature.name] = feature;
  });
  const BASE_INFO = {};
  function appendNavBar(child) {
    $(`#${enhanceNavId}`).append(child);
  }
  loadFeature("nav", false, function () {
    var $navBar = $("#bs-example-navbar-collapse-1");
    $navBar.append(`
        <ul id="${enhanceNavId}" class="nav navbar-nav navbar-right">
          
        </ul>
        `);
    return true;
  });
  const loadedJs = {};
  const srcMapping = {
    "bootstrap-switch": "https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap-switch.min.js",
    "bootstrap-switch.css": "https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap3/bootstrap-switch.min.css",
  };
  function require(deps) {
    deps = _.isArray(deps) ? deps : [deps];
    return Promise.all(deps.map(dep => loadJs(dep)))
  }
  function loadJs(src) {
    src = srcMapping[src] || src;
    if (_.endsWith(src, '.css')) {
      return loadCss(src)
    }
    if (loadedJs[src]) {
      return loadedJs[src];
    }
    const loader = new Promise(function (resolve, reject) {
      const gmAdd = GM_addElement("script", {
        src,
        type: "text/javascript"
      });
      if (gmAdd) {
        gmAdd.onload = function () {
          resolve();
        };
      } else {
        resolve();
      }
    });
    loadedJs[src] = loader;
    return loader;
  }
  function loadCss(href) {
    GM_addElement("link", {
      href,
      rel: "stylesheet",
    });
  }

  (function () {
    initFeatureId();
    initDiffModal();
    // 绑定复制事件
    $(document).on("click", "[data-copy]", function (e) {
      copy($(e.currentTarget).attr("data-copy-value")).then(function () {
        var $icon = $(e.target).parent().find(".glyphicon");
        $icon.removeClass("glyphicon-duplicate").addClass("glyphicon-ok");
        setTimeout(function () {
          $icon.addClass("glyphicon-duplicate").removeClass("glyphicon-ok");
        }, 2000);
      });
    });
    // 加载 layer  因为依赖 $ 所以在代码里面进行加载
    loadJs("https://cdn.jsdelivr.net/npm/[email protected]/src/layer.js");

    loadCss("https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap3/bootstrap-switch.min.css");
    loadJs("https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap-switch.min.js");
    const highlight_xcode_css = GM_getResourceText("highlight_xcode_css");
    const text_different_css = GM_getResourceText("text_different_css");
    GM_addStyle(highlight_xcode_css);
    GM_addStyle(text_different_css);

  })();

  function getAllFeaturenMap() {
    return allFeatureMap;
  }
  var onNamesacpeLoadedCbs = [];
  var firsetNamespaceLoaded = false;
  function onNamesacpeLoaded(cb) {
    if (typeof cb === "function") {
      onNamesacpeLoadedCbs.push(cb);
    }
    return {
      then: function (cb) {
        if (firsetNamespaceLoaded) {
          cb();
        }
        onNamesacpeLoadedCbs.push(cb);
      },
    };
  }
  loadFeature("onNamesacpeLoaded", true, function () {
    var $namespaces = $(".namespace-name");
    if ($namespaces.length == 0) {
      return false;
    }
    console.log("trigger namespaceLoaded");
    $("body").on("namespaceLoaded", () => {
      onNamesacpeLoadedCbs.forEach((cb) => cb());
    });
    $("body").trigger("namespaceLoaded");
    const observer = new MutationObserver(function () {
      console.log("rebuild", arguments);
      $("body").trigger("namespaceLoaded");
    });

    $.each($(".config-item-container"), (index, el) => {
      observer.observe(el, { childList: true });
    });
    firsetNamespaceLoaded = true;
  });
  function getAppId() {
    let hash = location.hash;
    if (hash) {
      hash = hash.substring(2);
      const url = new URL("http://localhost?" + hash);
      return url.searchParams.get("appid");
    }
  }

  function loadFeature(name, options, feature) {
    options =
      typeof options === "object"
        ? options
        : {
          switch: true,
          reloadOnHashChange: options,
        };
    if (options.switch) {
      //  allFeature.push(name);
      if (isFeatureDisabled(name)) {
        console.log(`loadFeature: ${name} has disabled`);
        return;
      }
    }
    var reloadOnHashChange = !!options.reloadOnHashChange;
    loadFeature0(name, feature, false);
    if (reloadOnHashChange) {
      $(window).on("hashchange", function (e) {
        console.log("hashchange");
        loadFeature0(name, feature, true);
      });
    }
  }

  function isFeatureDisabled(name) {
    var state = featureState(name);
    if (state === true) {
      // 明确设置过为启用
      return false;
    }
    if (state === false) {
      // 明确设置过为不启用
      return true;
    }
    // 默认开关
    if (allFeatureMap[name] && !allFeatureMap[name].defaultEnabled) {
      console.log(`loadFeature: ${name} has disabled by deafult`);
      return true;
    }
    return false;
  }
  function featureState(name, state) {
    return featureTypeState(name, null, state);
  }
  function featureTypeState(name, subtype, state) {
    var feature = subtype ? name + "-" + subtype : name;
    var stateMap = JSON.parse(localStorage.getItem("featureState")) || {};
    if (state == undefined) {
      // get
      return stateMap[feature];
    }
    //set
    stateMap[feature] = state;
    localStorage.setItem("featureState", JSON.stringify(stateMap));
  }
  function switchFeature(name, enabled) {
    featureState(name, enabled);
  }


  function copy(content) {
    return new Promise(function (res, rej) {
      let copy = function (e) {
        try {
          e.preventDefault();
          e.clipboardData.setData("text/plain", content);
          document.removeEventListener("copy", copy);
          console.log("copy value:", content);
          res();
        } catch (e) {
          rej();
        }
      };
      document.addEventListener("copy", copy);
      document.execCommand("Copy");
    });
  }

  function toPerttyJson(val) {
    try {
      return JSON.stringify(JSON.parse(val), null, 2);
    } catch (e) {
      return val;
    }
  }

  function loadFeature0(name, feature, isReloadByHash) {
    try {
      if (!isReloadByHash && $("#feature-" + name).length !== 0) {
        // 已经加载过了
        console.log(`loadFeature: ${name} has loaded`);
        return;
      }
      var clear = setInterval(function () {
        if (feature(isReloadByHash) !== false) {
          console.log(`loadFeature: ${name} finished`);
          $(`${"#" + featureId}`).append(`<div id="feature-${name}"></div>`);
          clearInterval(clear);
        }
      }, 1000);
    } catch (e) {
      console.error(`load feature failed :${name}`, e.message);
    }
  }

  function initFeatureId() {
    $("body").prepend(`<div id="${featureId}" class="hidden"></div>`);
  }

  function showDiffModal(key, newVal, oldVal) {
    const tdfh = new TextDifferentForHtml(
      $("#diff-container")[0], // The dom used to render the display code
      "json" // Type of code
    );
    $("#diff-detail-title").html(`${key}`);
    $("#copyOld").attr("data-copy-value", oldVal);
    $("#copyNew").attr("data-copy-value", newVal);
    tdfh.render({
      oldCode: toPerttyJson(oldVal), // Old code
      newCode: toPerttyJson(newVal), // New code
      hasLineNumber: false, // Whether to display the line number
    });
    $("#diffModal").modal();
  }
  function initDiffModal() {
    $("body").append(`
      <!-- Modal -->
      <div class="modal" id="diffModal" tabindex="-1" role="dialog" aria-labelledby="diffModal">
        <div class="modal-dialog modal-lg" role="document">
          <div class="modal-content">
            <div class="modal-header">
              <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
              <h4 class="modal-title"><span class="text-danger" id="diff-detail-title"></span> 差异对比</h4>
            </div>
            <div class="modal-body" >
                <div class="row">
                  <div class="col-xs-6 text-center">
                    <span data-tooltip="tooltip" title="点击复制" id="copyOld" data-copy="copy" data-copy-value=""  class="label label-default">旧值 
                    <label class="glyphicon glyphicon-duplicate"></label>
                    </span>
                  </div>
                  <div class="col-xs-6 text-center">
                    <span data-tooltip="tooltip" title="点击复制" id="copyNew" data-copy="copy" data-copy-value="" class="label label-success">新值
                    <label class="glyphicon glyphicon-duplicate"></label>
                    </span>
                  </div>
                </div>
                <div id="diff-container" style="display:flex"></div>
            </div>
          </div>
        </div>
      </div>
      `);
  }

  loadFeature("copyNamespace", false, function () {
    onNamesacpeLoaded(function () {
      $("header.panel-heading .header-namespace>span:first-child")
        .toArray()
        .forEach(function (el) {
          var name = el.innerText.trim();
          var $el = $(el);
          if ($el.nextAll(".copyNamespace").length != 0) {
            return;
          }
          $el.next("span").after(`
        <span data-tooltip="tooltip" title="点击复制namespace" data-copy="copy"
         data-copy-value="${name}" class="copyNamespace label label-success">复制
        <label class="glyphicon glyphicon-duplicate"></label>
        </span>
        `);
        });
    });

    return true;
  });

  loadFeature("fixEnvTab", true, function (isReloadByHash) {
      var $tab = $(".J_appFound");
      if ($tab.length == 0) {
        return false;
      }
      const infoVal = sessionStorage[getAppId()];
      const infoObj = JSON.parse(infoVal);
      infoObj.cluster;
      infoObj.env;
      var $panelHeader = $tab.find(".panel-heading:first");
      var $curEnvInfo = $panelHeader.find("#curEnvInfo");
      if ($curEnvInfo.length == 0) {
        $tab.find(".panel-heading>span").after(`
          <button type="button" class="slideBtn btn btn-primary btn-xs">(点击展开/收缩)</button>
      `);
        $panelHeader.append(`<div id="curEnvInfo"></div>`);
        $curEnvInfo = $panelHeader.find("#curEnvInfo");
      }

      var $scope = $('div[ng-controller="ConfigBaseInfoController"]').scope();
      var pageContext = $scope.pageContext;
      $curEnvInfo.html(`
    <span class="label label-success">${pageContext.env}</span> - <span class="label label-info">${pageContext.clusterName}</span>
    `);
      if (!isReloadByHash) {
        // 不是通过 hash change reload 的才需要绑定事件
        $tab.find(".panel-heading .slideBtn").on("click", function (e) {
          const $header = $(e.target).parent(".panel-heading");
          $header.next("div").slideToggle("normal", function () {});
        });

        $tab = $tab.parent();
        $tab.on("affixed.bs.affix", function (e) {
          $tab.css({ position: "fixed" });
          $tab
            .find(".panel-heading")
            .next("div")
            .slideUp("normal", function () {});
        });
        $tab.on("affixed-top.bs.affix	", function (e) {
          $tab.css({ position: "" });
          $tab
            .find(".panel-heading")
            .next("div")
            .slideDown("normal", function () {});
        });
        $tab.affix({
          offset: {
            top: 60,
          },
        });
      }
      return true;
    });

  loadFeature("fixNiceScroll", false, function () {
      $(document).ready(function () {
        $().niceScroll;
        $.prototype.niceScroll = function(){
          
        };
        // 放在初始化之后执行
        setTimeout(function () {
          $("html").css("overflow", "");
          var htmlScroll = $("html").getNiceScroll && $("html").getNiceScroll(0);
          htmlScroll && htmlScroll.remove();
        }, 200);
      });
      return true;
    });

  function scrollTo(el) {
      $(el)[0].scrollIntoView({ behavior: 'smooth' });
  }

  let inited = false;
  loadFeature("gotoNamespace", false, () => {
    if ($("#affixPlaceholder").length == 0) {
      $("body>nav.navbar").after(
        '<div id="affixPlaceholder" style="height:60px"></div>'
      );
      $("body>nav.navbar")
        .width("100%")
        .css({ "z-index": 999, position: "fixed" });
    }
    goToNamespace0();
    return true;
  });

  function goToNamespace0() {
    onNamesacpeLoaded().then(() => {
      if (!inited) {
        buildGotoNamespace();
        inited = true;
      }
      refreshGogoNamespace();
    });
  }

  function refreshGogoNamespace() {
    var $select = $("#namespaceSelecter");
    if ($select.hasClass("select2-hidden-accessible")) {
      // Select2 has been initialized
      $select.select2("destroy");
    }

    // init
    $select.select2({
      placeholder: "跳转到 Namespace",
      templateResult: formatOptions,
      templateSelection: formatOptions,
    });
  }
  function formatOptions(state) {
    if (state.disabled) {
      return state.text;
    }
    var namespaceScope = $(
      'div[ng-controller="ConfigNamespaceController"]'
    ).scope();
    var namespace = namespaceScope.namespaces.find((namespace) => {
      return namespace.viewName === state.id;
    });
    if (namespace.itemModifiedCnt > 0) {
      return $(
        `<label>${state.text} <span class="label label-warning ">改</span></label>`
      );
    }
    return state.text;
  }

  var compiled = _$1.template(`<% _.forEach(namespaces, function(namespace) { %>
  <option value="<%- namespace.viewName%>"><%- namespace.viewName %></option><% 
   }); 
  %>`);
  function buildGotoNamespace() {
    var $select = $("#namespaceSelecter");
    if ($select.length > 0) {
      return;
    }
    var namespaceScope = $(
      'div[ng-controller="ConfigNamespaceController"]'
    ).scope();
    var optionsTpl = compiled({ namespaces: namespaceScope.namespaces });
    appendNavBar(`
  <li id="goToNamespace" style="margin-top: 10px;">
  <select id="namespaceSelecter">${optionsTpl}</select>
  </li>
  `);
    $select = $("#namespaceSelecter");
    $select.on("select2:open", function (e) {
      $("#select2-namespaceSelecter-results").css({ "max-height": "600px" });
    });

    //var htmlScroll = $("html").getNiceScroll && $("html").getNiceScroll(0);
    // 修改选项时 滚动页面到对应位置
    $select.on("select2:select", function (e) {
      var namespaceId = $select.val();
      var namespaceEl = $(".namespace-name")
        .toArray()
        .find((el) => el.innerHTML == namespaceId);
      scrollTo(namespaceEl);
      //      htmlScroll && htmlScroll.doScrollTop($(namespaceEl).offset().top - 100, 1000);
    });

    // 滚动页面时同步改变 当前选择的 namespace 选项
    changeSelectedOnScroll($select);
  }

  function changeSelectedOnScroll($select) {
    var selectedVal;
    // 防抖
    var listener = _$1.debounce(function (entries) {
      if (entries.length == 0) {
        return;
      }
      var entry = entries[0];
      if (!entry.isIntersecting) {
        // 从可视区移出
        return;
      }
      var el = entry.target;
      var curNamespace = $(el).text();
      if (selectedVal != curNamespace) {
        selectedVal = curNamespace;
        $select.val(selectedVal).trigger("change");
      }
    }, 200);
    const io = new IntersectionObserver(listener, { threshold: 1.0 });

    $(".namespace-name").each((i, el) => {
      io.observe(el);
    });
  }

  loadFeature("prodWarnDisable", false, function () {
    prodWarnDisable();
  });

  function prodWarnDisable() {

    var $btn = $("#releaseModal div.modal-footer").find("button[type=submit]");
    $btn.click(function (e) {
      var namespaceScope = $(
        'div[ng-controller="ConfigNamespaceController"]'
      ).scope();
      var env = namespaceScope.pageContext.env;
      if (!isProd(env)) {
        return;
      }
      // var my = namespaceScope.$root.userName
      // var toReleaseNamespace = $(releaseForm).isolateScope()?.toReleaseNamespace;
      // var selfModify = true;
      // if (toReleaseNamespace) {
      //   selfModify = toReleaseNamespace.items.filter(item => item.isModified).find(item => item.item.dataChangeLastModifiedBy === my)
      // }
      if (!isFeatureDisabled("prodWarnDisable")) {
        if (confirm("已关闭生产环境发布校验,是否继续?")) {
          namespaceScope.$root.userName = "disabledProdWarn";
        }
      }
    });
  }
  function isProd(env) {
    return  env && env === "PRO";
  }

  var DiffMatch = new diff_match_patch();

  loadFeature("releaseDiff", false, function () {
    var releaseModalNode = document.querySelector("#releaseModal");
    if (releaseModalNode == null) {
      return false;
    }
    bindDiffInfo(releaseModalNode);
    return true;
  });

  function bindDiffInfo(node) {
    var observer = new MutationObserver(function () {
      initChangeInfoHeader();
      // 每次都需要隐藏
      initChangeInfoDetail();
      var $cols = $("#releaseModal table tr.ng-scope");

      var kvInfo = {};
      for (const col of $cols) {
        var $col = $(col);
        var tds = $(col).find("td");
        kvInfo = {
          key: tds[0].title,
          oldVal: tds[1].title,
          newVal: tds[2].title,
        };

        buildDiffHtml(
          $col.find("td.diff-text"),
          kvInfo.key,
          kvInfo.oldVal,
          kvInfo.newVal
        );
      }
    });
    observer.observe(node, {
      attributeFilter: ["style"],
    });
  }

  function toggleDiff() {
    $(".change-diff").toggle();
    var needShow = $(".change-diff").is(":hidden");
    if (needShow) {
      $(".change-detail").show();
    } else {
      $(".change-detail").hide();
    }
  }
  function initChangeInfoDetail() {
    $(".change-detail").hide();
    var $cols = $("#releaseModal table tr.ng-scope");
    for (var col of $cols) {
      var $col = $(col);
      if ($col.hasClass("diff-info-inited")) {
        return;
      }
      initChageCol();
      $col.addClass("diff-info-inited");
    }
  }
  function initChageCol() {
    var bodyRows = $("#releaseModal table tr.ng-scope");
    for (var row of bodyRows) {
      var $row = $(row);
      if ($row.find("td.change-diff").length == 0) {
        $row.find("td:gt(0)").addClass("change-detail x-detail").hide();
        $row.append(
          '<td class="change-diff diff-text" data-toggle="tooltip" data-placement="top" title="点击查看详细差异对比"></td>'
        );
      }
    }
    $(".change-diff.diff-text").tooltip();
  }
  function initChangeInfoHeader() {
    if ($("#releaseModal table thead tr>th").length == 0) {
      return;
    }
    if ($("#toggleDiff").length != 0) {
      return;
    }
    // 隐藏原有信息
    $("#releaseModal table thead tr>th:gt(0)").addClass("change-detail").hide();
    // 增加差异信息展示
    var headCol = $("#releaseModal table thead tr");
    headCol.append('<th class="change-diff">差异(点击查看新旧值对比)</th>');
    $("#releaseModal table thead tr>th:eq(0)").append(
      '<button id="toggleDiff">切换显示</button>'
    );
    $("#toggleDiff").click(function () {
      toggleDiff();
      return false;
    });
  }

  function buildDiffHtml($node, key, oldVal, newVal) {
    // 新增或删除

    var diff = DiffMatch.diff_main(oldVal, newVal);

    DiffMatch.diff_cleanupSemantic(diff);

    var html = DiffMatch.diff_prettyHtml(diff);
    $node.html(html);
    var errorJson = isErrorJson(newVal);
    if (errorJson) {
      var $td = $node.parent().find("td:first");
      var errorJsonLabelId = `${key}-errorJson`;
      if ($(`#${errorJsonLabelId}`).length == 0) {
        $td.append(
          `<span id="${errorJsonLabelId}" class="label label-danger">错误的json</span>`
        );
        $td.addClass("alert alert-danger");
      }
    }
    $node.on("click", function () {
      showDiffModal(key, newVal, oldVal);
    });
  }

  function isErrorJson(val) {
    val = val.trim();
    if (val.startsWith("{") || val.startsWith("[")) {
      try {
        JSON.parse(val);
        return false;
      } catch (e) {
        return true;
      }
    }
    return false;
  }

  loadFeature("releaseModal", true, function () {
    if ($("#goReleaseMoadlBottom").length == 0) {
      $('#releaseModal div.modal-header .modal-title:not(".ng-hide")').append(
        `<span id="goReleaseMoadlBottom" class="glyphicon glyphicon-circle-arrow-down" data-tooltip="tooltip" data-placement="top" title="定位到发布按钮"></span>`
      );
    }
    if ($("#goReleaseMoadlTop").length == 0) {
      $("#releaseModal div.modal-footer").prepend(`
    <span id="goReleaseMoadlTop" class="pull-left glyphicon glyphicon-circle-arrow-up" data-tooltip="tooltip" data-placement="top" title="回到顶部"></span>`);
    }
    // for scroll
    $("#goReleaseMoadlBottom").on("click", function () {
      scrollTo($("#goReleaseMoadlTop"));
    });

    $("#goReleaseMoadlTop").on("click", function () {
      scrollTo($("#goReleaseMoadlBottom"));
    });
    return true;
  });

  loadFeature(
    "settings",
    { switch: false, reloadOnHashChange: false },
    function () {
      require(["bootstrap-switch"])
        .then(() => {
          buildSettings();
        });
    }
  );
  function buildSettings() {
    initSettingsModal();
    $("[data-toggle=switch]")
      .bootstrapSwitch({
        onText: "开启",
        offText: "关闭",
        onSwitchChange: function (event, state) {
          var $el = $(this);
          var featureName = $el.val();
          var feature = getAllFeaturenMap()[featureName];
          if (
            feature &&
            state &&
            !featureTypeState(featureName, "enabledWarn") &&
            feature.enabledWarn
          ) {
            layer.confirm(
              feature.enabledWarn,
              { icon: 3, btn: ["确定", "取消"] },
              function (index) {
                switchFeature(featureName, true);
                featureTypeState(featureName, "enabledWarn", true);
                $el.bootstrapSwitch("state", true);
                layer.close(index);
                layer.confirm('切换成功,刷新生效。是否立即刷新页面?', function (idx) {
                  location.reload();
                });
              },
              function () { }
            );
            return false;
          } else {
            switchFeature(featureName, state);
            layer.confirm('切换成功,刷新生效。是否立即刷新页面?', function (idx) {
              location.reload();
            });
          }
        },
      });

    appendNavBar(`
  <li>
  <a href="javascript:void(0);" id="showSettings">
  <span class="glyphicon glyphicon-cog"></span>
  </a>
  </li>
  `);
    $("#showSettings").on("click", showSettings);
  }

  function showSettings() {
    $("#settingsModal").modal();
  }

  function initSettingsModal() {
    var tpl = "";
    var moreTpl = "";
    allFeature.forEach((feature) => {
      var key = feature.name.replace(".", "-");
      var checked = isFeatureDisabled(feature.name) ? "" : "checked";
      var _tpl = `
        <div class="form-group" style="width:45%;margin:5px 0px;">
            <label class="col-sm-6 control-label" for="feature-switch-${key}">${feature.name}
            <span class="glyphicon glyphicon-question-sign" data-tooltip="tooltip" title="${feature.desc}"></span>
            </label>
            <div class="col-sm-6">
            <input type="checkbox" data-toggle="switch" value="${feature.name}" id="feature-switch-${key}" ${checked}/>
            </div>
        </div>    
        `;
      if (feature.more) {
        moreTpl += _tpl;
      } else {
        tpl += _tpl;
      }
    });
    $("body").append(`
        <!-- Modal -->
        <div class="modal fade" id="settingsModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
          <div class="modal-dialog modal-lg" role="document">
            <div class="modal-content">
              <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                <h4 class="modal-title"><span class="text-danger" id="diff-detail-title"></span> 设置 (修改后刷新生效) 
                <a href="javascript:void(0);" id="showFeatureInfo">
                <span class="glyphicon glyphicon-question-sign"></span>
                </a>
                </h4>
              
              </div>
              <div class="modal-body" >
              <form class="form-inline">
              ${tpl}
              </form>
              <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
              <div class="panel panel-default">
                <div class="panel-heading" role="tab" id="headingOne">
                  <h4 class="panel-title">
                    <a role="button" data-toggle="collapse" data-parent="#accordion" 
                    onclick="$('#collapseOne').collapse('toggle');" aria-controls="collapseOne">
                      更多功能
                    </a>
                  </h4>
                </div>
                <div id="collapseOne" class="panel-collapse collapse out" role="tabpanel" aria-labelledby="headingOne">
                  <div class="panel-body">
                  <form class="form-inline">
                  ${moreTpl}
                  </form>
                  </div>
                </div>
              </div>
            </div>
              </div>
              <div class="modal-footer">
                <div><a href="https://greasyfork.org/zh-CN/scripts/447045-apollo-enhance" target="_blank" title="更新检测">${BASE_INFO.version}</a> </div>
                <div class="center-block">
                  企微反馈👉 <a href="wxwork://message?username=xizhouxi">@xizhouxi</a>
                </div>

              </div>
            </div>
          </div>
        </div>
        `);
  }

  loadFeature("showText", true, function () {
    var $namespaces = $(".namespace-view-table");
    if ($namespaces.length == 0) {
      return false;
    }
    var currItem;
    $("#showTextModal .modal-body")
      .tooltip({
        title: "点击查看差异对比",
      })
      .on("click", function () {
        if (currItem && currItem.isModified) {
          showDiffModal(currItem.item.key, currItem.newValue, currItem.oldValue);
        }
      });
      // 点击查看时 选择当前的 item
      $('body').on('click', "td.cursor-pointer", function(e){
        var $target = $(e.currentTarget);
        if($target.prev('td.cursor-pointer').length == 0){
          // 说明点击的是 key  忽略
          return;
        }
        var $tr = $target.parent();
        var key = $tr.find("td:eq(1)").find('span:eq(0)').text().trim();
          var namespace = $tr
            .parents("section.master-panel-body.ng-scope")
            .find("b.namespace-name.ng-binding")
            .text()
            .trim();
          var namespaceScope = $(
            'div[ng-controller="ConfigNamespaceController"]'
          ).scope();
          var namesapce = namespaceScope.namespaces.find(
            (e) => e.baseInfo.namespaceName === namespace
          );
          console.log('show key:', namespace, key);
          currItem = namesapce.items.find((e) => e.item.key === key);
         
      });
    
  });

  loadFeature("stash", true, function () {
    onNamesacpeLoaded(function () {
      console.log("stash");

      $.each(
        $("section.master-panel-body.ng-scope>.panel-heading>"),
        function (idx, el) {
          var $panel = $(el);
          if ($panel.find(".stashFeature").length > 0) {
            return;
          }
          $panel.append(`<label class="stashFeature hidden"/>`);
          var namespaceName = $panel
            .find("b.namespace-name.ng-binding")
            .text()
            .trim();
          var namespaceScope = $(
            'div[ng-controller="ConfigNamespaceController"]'
          ).scope();
          var ConfigService = angular
            .injector(["application"])
            .get("ConfigService");
          var namespace = namespaceScope.namespaces.find(
            (e) => e.baseInfo.namespaceName === namespaceName
          );
          var $menu = $(el).find(".dropdown-menu");
          $menu.append('<li role="separator" class="divider"></li>');
          $(`
            <li><a href="javascript:void(0);" ><span class="glyphicon glyphicon-save"></span>暂存改动</a></li>
            `)
            .on("click", function () {
              layer.confirm("是否要暂存改动?", function (index) {
                console.log(namespaceName);
                console.log(namespace);
                console.log(ConfigService);
                var items = namespace.items.filter((item) => item.isModified);
                if (items.length === 0) {
                  layer.close(index);
                  layer.msg("没有改动");
                  return;
                }
                localStorage.setItem(
                  getStashKey(namespaceScope, namespace),
                  JSON.stringify(items)
                );

                // recovery old value
                Promise.all(
                  items.map((item) => {
                    var isNewValue = item.isModified && !item.oldValue;
                    var isDeleted = item.isDeleted;
                    if (isNewValue) {
                      // 新增的配置 删除
                      return ConfigService.delete_item(
                        namespace.baseInfo.appId,
                        namespaceScope.pageContext.env,
                        namespace.baseInfo.clusterName,
                        namespace.baseInfo.namespaceName,
                        item.item.id
                      );
                    }
                    if (isDeleted) {
                      // 被删除的数据 拿不到原来的 item 信息
                      // 那就 create
                      return ConfigService.create_item(
                        namespace.baseInfo.appId,
                        namespaceScope.pageContext.env,
                        namespace.baseInfo.clusterName,
                        namespace.baseInfo.namespaceName,
                        {
                          key: item.item.key,
                          value: item.oldValue,
                          tableViewOperType: "create",
                          addItemBtnDisabled: true,
                        }
                      );
                    }
                    // 更新的 key
                    return ConfigService.update_item(
                      namespace.baseInfo.appId,
                      namespaceScope.pageContext.env,
                      namespace.baseInfo.clusterName,
                      namespace.baseInfo.namespaceName,
                      $.extend(item.item, {
                        value: item.oldValue,
                        tableViewOperType: "update",
                      })
                    );
                  })
                ).then(() => {
                  layer.close(index);
                  layer.msg("改动暂存成功!");
                  // 直接 reload
                  location.reload();
                });
              });
            })
            .appendTo($menu);

          $(`
            <li><a href="javascript:void(0);"><span class="glyphicon glyphicon-open"></span>恢复暂存改动</a></li>
            `)
            .on("click", function () {
              var items = JSON.parse(
                localStorage.getItem(getStashKey(namespaceScope, namespace))
              );
              if (!items || items.length === 0) {
                layer.msg("没有暂存数据");
                return;
              }
              layer.open({
                content: getUnstashTable(items),
                yes: function (index) {
                  // recovery old value
                  Promise.all(
                    items.map((item) => {
                      var isNewValue = item.isModified && !item.oldValue;
                      var isDeleted = item.isDeleted;
                      if (isDeleted) {
                        // 重新拿一次 itemid
                        item = namespace.items.filter(
                          (v) => v.item.key === item.item.key
                        )[0]; // 取第一个
                        // 新增的配置 删除
                        return ConfigService.delete_item(
                          namespace.baseInfo.appId,
                          namespaceScope.pageContext.env,
                          namespace.baseInfo.clusterName,
                          namespace.baseInfo.namespaceName,
                          item.item.id
                        );
                      }
                      if (isNewValue) {
                        // 被删除的数据 拿不到原来的 item 信息
                        // 那就 create
                        return ConfigService.create_item(
                          namespace.baseInfo.appId,
                          namespaceScope.pageContext.env,
                          namespace.baseInfo.clusterName,
                          namespace.baseInfo.namespaceName,
                          {
                            key: item.item.key,
                            value: item.newValue,
                            tableViewOperType: "create",
                            addItemBtnDisabled: true,
                          }
                        );
                      }
                      // 更新的 key
                      return ConfigService.update_item(
                        namespace.baseInfo.appId,
                        namespaceScope.pageContext.env,
                        namespace.baseInfo.clusterName,
                        namespace.baseInfo.namespaceName,
                        $.extend(item.item, {
                          value: item.newValue,
                          tableViewOperType: "update",
                        })
                      );
                    })
                  ).then(() => {
                    layer.close(index);
                    layer.msg("恢复暂存成功!");
                    localStorage.removeItem(
                      getStashKey(namespaceScope, namespace)
                    );
                    // 直接 reload
                    location.reload();
                  });
                },
              });
            })
            .appendTo($menu);
          console.log("add stash", namespaceName);
        }
      );
    });
  });

  function getStashKey(namespaceScope, namespace) {
    var baseInfo = namespace.baseInfo;
    return `stash_${namespaceScope.pageContext.env}_${baseInfo.appId}_${baseInfo.clusterName}_${baseInfo.namespaceName}`;
  }
  function getUnstashTable(changedItems) {
    var tab = "";
    for (var item of changedItems) {
      tab += `<tr>
          <td>${item.item.key}</td>
          <td data-value="${item.newValue}">
          ${
            item.newValue.length < 150
              ? item.newValue
              : item.newValue.substring(0, 150) + "..."
          }
          </td>
          <td>${item.item.comment ? item.item.comment : ""}</td>
          <td>${
            item.item.dataChangeLastModifiedTime
              ? item.item.dataChangeLastModifiedTime
              : ""
          }</td>
          </tr>`;
    }
    return `
    <table class="table table-bordered table-striped table-hover">
      <thead>
      <tr>
          <th>
              Key
          </th>
          <th>
              new Value
          </th>
          <th>
              备注
          </th>
          <th>
              最后修改时间
          </th>
          </tr>
          </thead>
          <tbody id="unstashTable">
              ${tab}
          </tbody>
          </table>`;
  }

  const DateType = {
      isJson(val) {
          try {
              JSON.parse(val);
              return true
          } catch (e) {
              return false
          }

      }
  };

  const cm_modules = {
      core: {
          name: 'core',
          js: "https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/codemirror.min.js",
          css: "https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/codemirror.min.css"
      },
      mode: {
          json: {
              alias: "javascript",
              mode: "application/json",
              addons: ['json-lint']
          },
          javascript: {
              js: "https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/mode/javascript/javascript.min.js",
              mode: "application/javascript",
              addons: ['matchbrackets']
          }
      },
      addon: {
          dialog: {
              preload: true,
              css: "https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/addon/dialog/dialog.min.css",
              js: "https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/addon/dialog/dialog.min.js"
          },
          panel: {
              preload: true,
              js: "https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/addon/display/panel.min.js"
          },
          matchbrackets: { preload: true, js: "https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/addon/edit/matchbrackets.min.js" },
          foldcode: { js: "https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/addon/fold/foldcode.min.js" },
          foldgutter: {
              preload: true,
              js: "https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/addon/fold/foldgutter.min.js",
              css: "https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/addon/fold/foldgutter.min.css"
          },
          "indent-fold": {
              preload: true,
              js: "https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/addon/fold/indent-fold.min.js"
          },
          "json-lint": {
              js: "https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/addon/lint/json-lint.min.js"
          },
          "active-line": {
              preload: true,
              js: "https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/addon/selection/active-line.min.js"
          },
          "annotatescrollbar": {
              preload: true,
              js: "https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/addon/scroll/annotatescrollbar.min.js"
          },
          "search": {
              preload: true,
              js: "https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/addon/search/search.js"
          },
          "searchcursor": {
              preload: true,
              js: "https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/addon/search/searchcursor.min.js"
          },
          "matchesonscrollbar": {
              preload: true,
              css: "https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/addon/search/matchesonscrollbar.min.css",
              js: "https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/addon/search/matchesonscrollbar.min.js"
          },
          "match-highlighter": {
              preload: true,
              js: "https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/addon/search/match-highlighter.min.js"
          },
          "jump-to-line": {
              preload: true,
              js: "https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/codemirror/5.65.2/addon/search/jump-to-line.min.js"
          },
          "simplescrollbars": {
              preload: true,
              css: "https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/codemirror/5.65.2/addon/scroll/simplescrollbars.css",
              js: "https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-y/codemirror/5.65.2/addon/scroll/simplescrollbars.min.js"
          }
      }
  };
  const preLoadModule = [];
   (function () {
      ['mode', 'addon'].forEach(type => {
          for (let module of Object.keys(cm_modules[type])) {
              const modeO = cm_modules[type][module];
              modeO.name = module;
              if (modeO.preload) {
                  preLoadModule.push(modeO);
              }
          }
      });
  })();
  const panels = [];
  var cm = {
      detectMode(val) {
          let mode;
          if (DateType.isJson(val)) {
              mode = 'json';
          }
          return mode;
      },
      detectModeAndLoad(val) {
          const mode = this.detectMode(val);
          if (!mode) {
              return Promise.resolve();
          }
          return this._getAndloadMode(mode).then((modeO) => modeO?.mode)
      },
      init(cmObj) {
          cmObj.setOption("scrollbarStyle", "overlay");
          const panel = cmObj.addPanel($(`
        <div style="min-height:20px" class="clearfix">
            <div class="pull-right">
                <button class="btn btn-xs" type="button" data-cm-edit-type="format">json格式化</button>
                <button class="btn btn-xs" type="button" data-cm-edit-type="zip">json压缩</button>
            </div>
        </div>
        `)[0], { position: 'top', stable: true });
          panels.push(panel);
          $(document).on('click', 'button[data-cm-edit-type]', function (event) {
              const type = $(this).attr('data-cm-edit-type');
              let value = cmObj.getValue();
              const isJson = DateType.isJson(value);
              if (!isJson) {
                  return
              }
              switch (type) {
                  case 'format':
                      value = JSON.stringify(JSON.parse(value), null, 2);
                      break
                  case 'zip':
                      value = JSON.stringify(JSON.parse(value));
                      break
              }
              cmObj.setValue(value);
          });
      },
      destory(cmObj) {
          for (const panel of panels) {
              panel.clear();
          }
          cmObj && cmObj.toTextArea();
      },
      autoMode(cmObj) {
      },
      _getAndloadMode(mode) {
          let modeO = cm_modules.mode[mode];
          if (!modeO) {
              console.error(`不支持的mode:${mode}`);
              return Promise.resolve()
          }
          const alias = modeO.alias;
          if (alias) {
              return this._loadModeAddon(modeO).then(() => this._getAndloadMode(alias))
          } else {
              return this._loadModeAddon(modeO).then(() => this._loadModule(modeO))
          }
      },
      _loadModule(moduleO) {
          if (moduleO.loaded) {
              return Promise.resolve(moduleO);
          }
          moduleO.css && loadCss(moduleO.css);
          return loadJs(moduleO.js)
              .then(() => {
                  moduleO.loaded = true;
                  return moduleO;
              })
      },
      _loadAddon(addOn) {
          const addonO = cm_modules.addon[addOn];
          return this._loadModule(addonO)
      },
      _loadModeAddon(modeO) {
          if (modeO.addons) {
              return Promise.all(modeO.addons.map(addon => this._loadAddon(addon)))
          }
          return Promise.resolve()

      }
  };

  $(function () {
      cm._loadModule(cm_modules.core).then(() => {
          preLoadModule.forEach(moduleO => {
              cm._loadModule(moduleO);
          });
      });
  });

  loadFeature("valueCodeEditor", false, function () {
      let cmObj;
      function sync() {
          if (cmObj) {
              cmObj.save();
              $(cmObj.getTextArea()).trigger('change'); // 触发angular.js 
          }
      }
      $("#itemModal")
          .on("shown.bs.modal", function () {
              const $textarea = $('#itemModal textarea[name=value]');
              cm.detectModeAndLoad($textarea.val())
                  .then(mode => {
                      console.log(mode);
                      cmObj = CodeMirror.fromTextArea($textarea[0], {
                          lineNumbers: true,
                          lineWrapping: true,
                          styleActiveLine: true,
                          mode: mode
                      });
                      cmObj.setSize('auto', '500px');
                      cmObj.on('changes', function () {
                          sync();
                      });
                      cm.init(cmObj);
                  });

              //$("html").css("overflow", "hidden");


              //htmlScroller.hide();
          })
          .on("hidden.bs.modal", function () {
              cm.destory(cmObj);
              
              console.log(cmObj);
          });
      var $btn = $("#itemModal div.modal-footer").find("button[type=submit]");
      $btn.click(function (e) {
          sync();
      });

  });

  BASE_INFO.version = "0.9.16";
  loadFeature("main", { switch: false }, function () {
    $("body").trigger("featureLoaded");
    console.log("trigger featureLoaded  v:", BASE_INFO);
  });

})(_);