Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
ImportExportDropDownTools.vue.js 11.83 KiB
/* eslint-disable max-lines */
// @ts-check

import Vue from "vue";
import { mapState } from 'vuex';
import { BaseLogger as logger } from '@cern/base-vue';
import { assign, cloneDeep, difference, first, forEach, get, groupBy, has, indexOf,
  intersection, isNil, keys, mapValues, merge, omit, set, toNumber,
  transform } from 'lodash';
import { ChannelParamsMap, ZSPMode } from '../../interfaces/daq';
import { AreaListDnsMap } from '../../Consts';
import { parseValue } from '../../interfaces';
/**
 * @typedef {{
 *  fileInput: HTMLInputElement,
 *  importDialog: V.Instance<typeof BaseVue.BaseDialog>
 * }} Refs
  * @typedef {V.Instance<typeof component, V.ExtVue<any, Refs>> } Instance
 */

const component = /** @type {V.Constructor<any, Refs>} */ (Vue).extend({
  name: 'ImportExportDropDownTools',
  props: {
    sources: { type: Object, default: null }, // DaqConfigSource
    isConfiguring: { type: Boolean, default: false }
  },
  /**
   * @returns {{
   *   confFile?: File,
   *   configuration?: Object,
   *   dragOver: boolean,
   *   warnings: {[daq: string]: string}
   * }}
   */
  data() {
    return {
      confFile: undefined,
      configuration: undefined,
      dragOver: false,
      warnings: {}
    };
  },
  computed: {
    ...mapState([ 'dns' ]),
    ...mapState('eacs', [ 'daq', 'daqList', 'cardList' ])
  },
  methods: {

    /**
     * EXPORT
     */

    /** @this {Instance} */
    generateJSON() {
      const dataObj = {};
      forEach(mapValues(this.daqList, (/** @type{any} */ d) => this.daq[d.name]),
        (daqConf, daqName) => {
        // We remove edit object used in edit mode
          set(dataObj, daqName, omit(daqConf, [ 'edit', 'zsp.edit' ]));
        });
      return dataObj;
    },
    /** @this {Instance} */
    exportJSON() {
      const dataObj = this.generateJSON();
      this.downloadJson(dataObj);
    },
    /** @this {Instance} */
    exportCSV() {
      const jsonConf = this.generateJSON();
      /** @type Array<any> */
      const flattenObj = [];
      forEach(jsonConf, (daqConf, daqName) => {
        const channelsConf = get(daqConf, 'channels');
        const zspConf = get(daqConf, 'zsp');
        const zspInfo = {
          zspMode: get(zspConf, 'mode'),
          zspMaster: null
        };
        // If zspMode is 1 (singleMaster), get the master
        if (zspInfo.zspMode === ZSPMode.SINGLE_MASTER) {
          const masterId = first(keys(get(zspConf, 'master')));
          set(zspInfo, 'zspMaster', masterId);
        }

        transform(channelsConf, (res, cConf) =>  {
          const obj = merge({ daq: daqName }, cConf, cloneDeep(zspInfo));
          // Add zspMaster information only if mode is multimaster
          if (obj.zspMode === ZSPMode.MASTER) {
            if (indexOf(keys(get(zspConf, 'master')), cConf.id) !== -1) {
              // Case Master:
              set(obj, 'zspMaster', cConf.id);
            }
            else {
              // Case Slave
              const masterId = get(zspConf, [ 'channels', cConf.id, 'masterId' ]);
              // if is null, it means is independent.
              if (!isNil(masterId)) set(obj, 'zspMaster', masterId);
            }
          }
          res.push(obj);
        }, flattenObj);
      });
      const csv = this.jsonToCsv(flattenObj);
      this.downloadCsv(csv);
    },
    /**
     * @this {Instance}
     * @param {any} jsonArray
     */
    jsonToCsv(jsonArray) {
      const header = keys(first(jsonArray));
      /**
       * @param {string} key
       * @param {any} value
       * @returns {any}
       */
      const replacer = (key, value) => {
        if (value === true || value === false) {
          return toNumber(value);
        }
        else return isNil(value) ? '' : value;
      };
      const csv = [
        header.join(','),
        // @ts-ignore
        ...jsonArray.map((row) => header.map((field) => JSON.stringify(row[field], replacer)).join(','))
      ].join('\n');
      return csv;
    },
    /**
     * @param {string} csv
     * @returns {any}
     */
    csvToJson(csv) {
      const lines = csv.split('\n');
      const result = [];
      const headers = first(lines)?.split(',');
      if (isNil(headers)) return;
      for (let i = 1; i < lines.length; i++) {
        if (!lines[i]) continue;
        const obj = {};
        const currentline = lines[i].split(',');

        for (let j = 0; j < headers.length; j++) {
          const val = currentline[j].replace(/['"]+/g, '');
          const name = headers[j];
          const parsedValue = parseValue(val,
            get(ChannelParamsMap, [ name, 'type' ]));
          set(obj, name, parsedValue);
        }
        result.push(obj);
      }
      return result;
    },
    /**
     * @param {any} jsonObj json object to export
     */
    downloadJson(jsonObj) {
      var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(jsonObj));
      this.downloadFile(dataStr, 'json');
    },
    /**
     * @param {string} csv
     */
    downloadCsv(csv) {
      var dataStr = "data:text/csv;charset=utf-8," + encodeURIComponent(csv);
      this.downloadFile(dataStr, 'csv');
    },
    /**
     * @param {string} dataStr
     * @param {'json' | 'csv'} extension
     */
    downloadFile(dataStr, extension) {
      const area = get(AreaListDnsMap, this.dns, 'UNKNOWN');
      const fileName = `daq-config-${area}-${new Date().toISOString()}.${extension}`;
      var downloadAnchorNode = document.createElement('a');
      downloadAnchorNode.setAttribute("href", dataStr);
      downloadAnchorNode.setAttribute("download", fileName);
      document.body.appendChild(downloadAnchorNode); // required for firefox
      downloadAnchorNode.click();
      downloadAnchorNode.remove();
    },

    /**
     * IMPORT
     */

    /** @this {Instance} */
    async openImportDialog() {
      try {
        this.remove();
        await this.$refs.importDialog.request();
      }
      catch (err) { /* do nothing */}
    },
    /** @this {Instance} */
    async onChange() {
      assign(this, this.$options.data()); /* reset state */
      this.confFile = first(this.$refs.fileInput.files);
      if (isNil(this.confFile)) return;
      const confFileContent = await this.confFile.text();
      if (isNil(confFileContent)) return;
      // Check Extension
      // @ts-ignore
      const fileType = this.confFile.name.split('.').pop().toLowerCase();
      if (fileType === 'json')
        this.importJSON(confFileContent);
      else if (fileType === 'csv')
        this.importCSV(confFileContent);
      else
        logger.error('File not valid: ' + this.confFile.name);
    },
    /**
     * @param {string} importJsonStr
     */
    async importJSON(importJsonStr) {
      try {
        this.configuration = JSON.parse(importJsonStr);
        this.doChecks();
      }
      catch (e) {
        logger.error('Daq configuration file content is invalid: ' + this.confFile?.name);
      }
    },
    /**
     * @param {string} importCsvStr
     */
    async importCSV(importCsvStr) {
      // Translate CSV to JSON configuration
      this.configuration = {};
      const jsonObj = this.csvToJson(importCsvStr);
      // Group by daq name
      const gJsonObj = groupBy(jsonObj, 'daq');
      transform(gJsonObj, (res, daqConf, daqName) => {
        // Recreate zsp configuration
        const zspChannels = transform(daqConf, (resZps, channelConf) => {
          const zspObj = {
            sn: channelConf.serialNumber,
            channel: toNumber(channelConf.index),
            id: channelConf.id
          };
          set(resZps, channelConf.id, zspObj);
        }, {});

        const zspMode = toNumber(first(daqConf).zspMode);
        if (zspMode === ZSPMode.SINGLE_MASTER) {
          const masterId = first(daqConf).zspMaster;
          set(zspChannels, [ masterId, 'slave' ], []);
        }
        else if (zspMode === ZSPMode.MASTER) {
          forEach(daqConf, (cConf) => {
            const masterId = get(cConf, 'zspMaster');
            if (masterId !== cConf.id) {
              set(zspChannels, [ cConf.id, 'masterId' ], masterId);
              const masterSlaves = get(zspChannels, [ masterId, 'slave' ], []);
              masterSlaves.push(cConf.id);
              set(zspChannels, [ masterId, 'slave' ], masterSlaves);
            }
          });
        }

        // Reconstruct zsp update object
        const zspConf = {
          mode: zspMode,
          channels: zspChannels
        };
        // Reconstruct channels object
        const channelsConf = transform(daqConf, (res, channelConf) => {
          const newCc = omit(channelConf, [ 'daq', 'zspMode', 'zspMaster' ]);
          set(res, channelConf.id, newCc);
        }, {});
        set(res, daqName, {
          zsp: zspConf,
          channels: channelsConf
        });
      }, this.configuration);

      this.doChecks();
    },
    doChecks() {
      // check daqs are there by matching with daqList
      const daqs = keys(this.configuration);
      const daqList = keys(this.daqList);
      const unknownDaqs = difference(daqs, daqList);
      const missingDaqs = difference(daqList, daqs);
      forEach(missingDaqs, (daq) => {
        Vue.set(this.warnings, daq, "Missing daq configuration. Current configuration will be used.");
      });
      forEach(unknownDaqs, (daq) => {
        Vue.set(this.warnings, daq, "Unknown daq or not installed in current area. It will be ignored.");
      });
      /** @type {{[card: string]: string}} */
      const cardsInfo = {};
      const commonDaqs = intersection(daqs, daqList);
      forEach(this.configuration, (daq, daqName) => {
        if (indexOf(commonDaqs, daqName) === -1) return;
        const channels = get(daq, 'channels');
        forEach(mapValues(channels), (c) => set(cardsInfo, c.serialNumber, daqName));
      });
      const cards = keys(cardsInfo);
      const cardList = keys(this.cardList);
      const unknownCards = difference(cards, cardList);
      const missingCards = difference(cardList, cards);
      forEach(missingCards, (card) => {
        const daqName = get(this.cardList, [ card, 'daq' ]);
        Vue.set(this.warnings, `${daqName} > ${card}`,
          "Missing card configuration. Current configuration will be used.");
      });
      forEach(unknownCards, (card) => {
        const daqName = get(cardsInfo, card);
        Vue.set(this.warnings, `${daqName} > ${card}`,
          "Unknown card or not installed. It will be ignored.");
      });
    },
    remove() {
      assign(this, this.$options.data()); /* reset state */
    },
    /**
     * @this {Instance}
     * @param {DragEvent} event
     */
    dragover(event) {
      event.preventDefault();
      this.dragOver = true;
    },
    /**
     * @this {Instance}
     */
    dragleave() {
      this.dragOver = false;
    },
    /**
     * @this {Instance}
     * @param {DragEvent} event
     */
    drop(event) {
      event.preventDefault();
      if (isNil(event.dataTransfer)) return;
      this.$refs.fileInput.files = event.dataTransfer.files;
      this.onChange(); // Trigger the onChange event manually
      // Clean up
      this.dragleave();
    },
    /** @this {Instance} */
    async applyConf() {
      // Load json form file
      forEach(this.configuration, (conf, daq) => {
        if (!has(this.daqList, daq)) return;
        const channelsConf = get(conf, 'channels');
        const zspConf = get(conf, 'zsp');
        if (channelsConf && zspConf) {
          // Update the channels
          this.$store.commit('eacs/daq/' + daq + '/resetEdit');
          forEach(channelsConf, (cc, cardSnId) => {
            forEach(cc, (value, name) => {
              this.$store.commit('eacs/daq/' + daq + '/editChannel',
                { id: cardSnId, name, value });
            });
          });
          // Update zsp
          this.$store.commit('eacs/daq/' + daq + '/zsp/resetEdit');
          this.$store.commit('eacs/daq/' + daq + '/zsp/update', zspConf);
        }
      });
      // Now trigger configure
      this.$emit('configure');
    }
  }
});
export default component;