OpenC3/cosmos

View on GitHub
openc3-cosmos-init/plugins/packages/openc3-tool-common/src/tools/admin/PluginDialog.vue

Summary

Maintainability
Test Coverage
<!--
# Copyright 2022 Ball Aerospace & Technologies Corp.
# All Rights Reserved.
#
# This program is free software; you can modify and/or redistribute it
# under the terms of the GNU Affero General Public License
# as published by the Free Software Foundation; version 3 with
# attribution addendums as found in the LICENSE.txt
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.

# Modified by OpenC3, Inc.
# All changes Copyright 2024, OpenC3, Inc.
# All Rights Reserved
#
# This file may also be used under the terms of a commercial license
# if purchased from OpenC3, Inc.
-->

<template>
  <v-dialog persistent v-model="show" width="80vw">
    <v-card>
      <v-card-text>
        <v-card-title>{{ pluginName }} </v-card-title>
        <v-row v-if="existingPluginTxt !== null" class="notice d-flex flex-row">
          <v-icon x-large left color="yellow">mdi-alert-box</v-icon>
          <div style="flex: 1">
            The existing plugin.txt is different from the {{ pluginName }}'s
            plugin.txt. Navigate the diffs making whatever edits you want before
            installing. You may want to update {{ pluginName }}'s plugin.txt
            going forward.
          </div>
        </v-row>
        <v-row class="pb-3 pr-3">
          <v-tabs v-model="tab" class="ml-3">
            <v-tab :key="0"> Variables </v-tab>
            <v-tab v-if="existingPluginTxt === null" :key="1">
              plugin.txt
            </v-tab>
            <v-tab v-else :key="1"> plugin.txt </v-tab>
          </v-tabs>
        </v-row>
        <form v-on:submit.prevent="submit">
          <v-tabs-items v-model="tab">
            <v-tab-item :key="0" eager="true" class="tab">
              <div class="pa-3">
                <v-row class="mt-3">
                  <div v-for="(value, name) in localVariables" :key="name">
                    <v-col style="width: 220px">
                      <v-text-field
                        clearable
                        type="text"
                        :label="name"
                        v-model="localVariables[name]"
                      />
                    </v-col>
                  </div>
                </v-row>
              </div>
            </v-tab-item>
            <v-tab-item
              v-if="existingPluginTxt === null"
              :key="1"
              eager="true"
              class="tab"
            >
              <v-row class="pa-3"
                ><v-col>This can be edited before installation.</v-col></v-row
              >
              <pre ref="editor" class="editor"></pre>
            </v-tab-item>
            <v-tab-item v-else :key="1" eager="true" class="tab">
              <v-row class="pa-3"
                ><v-col
                  >Existing plugin.txt. This can be edited before
                  installation.</v-col
                ><v-col class="ml-6"
                  >Uneditable plugin.txt from the new plugin.</v-col
                ></v-row
              >
              <pre ref="editor" class="editor"></pre>
            </v-tab-item>
          </v-tabs-items>

          <!-- <v-row class="pt-5"> -->
          <v-card-actions class="mt-2">
            <div v-if="existingPluginTxt !== null">
              <v-btn color="primary" @click="nextDiff" class="mr-2"
                >Next Diff</v-btn
              >
              <v-btn color="primary" @click="previousDiff">Previous Diff</v-btn>
            </div>
            <v-spacer />
            <v-btn
              @click.prevent="close"
              outlined
              class="mx-2"
              data-test="edit-cancel"
            >
              Cancel
            </v-btn>
            <v-btn
              class="mx-2"
              color="primary"
              type="submit"
              data-test="edit-submit"
            >
              Install
            </v-btn>
          </v-card-actions>
          <!-- </v-row> -->
        </form>
      </v-card-text>
    </v-card>
  </v-dialog>
</template>

<script>
import * as ace from 'ace-builds'
import 'ace-builds/src-min-noconflict/mode-ruby'
import 'ace-builds/src-min-noconflict/theme-twilight'
import 'ace-builds/src-min-noconflict/ext-language_tools'
import 'ace-builds/src-min-noconflict/ext-searchbox'
import AceDiff from '@openc3/ace-diff'
import '@openc3/ace-diff/dist/ace-diff.min.css'
import '@openc3/ace-diff/dist/ace-diff-dark.min.css'

export default {
  props: {
    pluginName: {
      type: String,
      required: true,
    },
    variables: {
      type: Object,
      required: true,
    },
    pluginTxt: {
      type: String,
      required: true,
    },
    existingPluginTxt: {
      type: String,
      required: false,
    },
    value: Boolean, // value is the default prop when using v-model
  },
  data() {
    return {
      tab: 0,
      localVariables: [],
      localPluginTxt: '',
      localExistingPluginTxt: null,
      editor: null,
      differ: null,
    }
  },
  mounted() {
    const pluginMode = this.buildPluginMode()
    if (this.existingPluginTxt === null) {
      this.editor = ace.edit(this.$refs.editor)
      this.editor.setTheme('ace/theme/twilight')
      this.editor.session.setMode(new pluginMode())
      this.editor.session.setTabSize(2)
      this.editor.session.setUseWrapMode(true)
      this.editor.$blockScrolling = Infinity
      this.editor.setHighlightActiveLine(false)
      this.editor.setValue(this.localPluginTxt)
      this.editor.clearSelection()
      this.editor.focus()
    } else {
      this.tab = 1 // Show the diff right off the bat
      this.differ = new AceDiff({
        element: this.$refs.editor,
        mode: new pluginMode(),
        theme: 'ace/theme/twilight',
        left: {
          content: this.localExistingPluginTxt,
          copyLinkEnabled: false,
        },
        right: {
          content: this.localPluginTxt,
          editable: false,
        },
      })
      // Match our existing editors
      this.differ.getEditors().left.setFontSize(16)
      this.differ.getEditors().right.setFontSize(16)
      this.curDiff = -1 // so the first will be 0
    }
  },
  beforeDestroy() {
    if (this.editor) {
      this.editor.destroy()
    }
    if (this.differ) {
      this.differ.destroy()
    }
  },
  computed: {
    show: {
      get() {
        return this.value
      },
      set(value) {
        this.$emit('input', value) // input is the default event when using v-model
      },
    },
  },
  watch: {
    value: {
      immediate: true,
      handler: function () {
        this.localVariables = JSON.parse(JSON.stringify(this.variables)) // deep copy
        this.localPluginTxt = this.pluginTxt.slice()
        if (this.existingPluginTxt !== null) {
          this.localExistingPluginTxt = this.existingPluginTxt.slice()
        }
      },
    },
  },
  methods: {
    previousDiff() {
      this.curDiff--
      if (this.curDiff < 0) {
        this.curDiff = this.differ.diffs.length - 1
      }
      this.scrollToCurDiff()
    },
    nextDiff() {
      this.curDiff++
      if (this.curDiff >= this.differ.diffs.length) {
        this.curDiff = 0
      }
      this.scrollToCurDiff()
    },
    scrollToCurDiff() {
      if (this.differ.diffs.length === 0) return
      let lrow = this.differ.diffs[this.curDiff].leftStartLine
      let rrow = this.differ.diffs[this.curDiff].rightStartLine
      // Give it a little breathing room
      if (lrow > 5) {
        lrow -= 5
      }
      if (rrow > 5) {
        rrow -= 5
      }
      this.differ.getEditors().left.scrollToLine(lrow)
      this.differ.getEditors().right.scrollToLine(rrow)
    },
    buildPluginMode() {
      var oop = ace.require('ace/lib/oop')
      var RubyHighlightRules = ace.require(
        'ace/mode/ruby_highlight_rules',
      ).RubyHighlightRules

      // TODO: Grab from code
      let keywords = ['VARIABLE']
      let regex = new RegExp(`(\\b${keywords.join('\\b|\\b')}\\b)`)
      var PluginHighlightRules = function () {
        RubyHighlightRules.call(this)
        // add openc3 rules to the ruby rules
        for (var rule in this.$rules) {
          this.$rules[rule].unshift({
            regex: regex,
            token: 'support.function',
          })
        }
      }
      oop.inherits(PluginHighlightRules, RubyHighlightRules)

      var MatchingBraceOutdent = ace.require(
        'ace/mode/matching_brace_outdent',
      ).MatchingBraceOutdent
      var CstyleBehaviour = ace.require(
        'ace/mode/behaviour/cstyle',
      ).CstyleBehaviour
      var FoldMode = ace.require('ace/mode/folding/ruby').FoldMode
      var Mode = function () {
        this.HighlightRules = PluginHighlightRules
        this.$outdent = new MatchingBraceOutdent()
        this.$behaviour = new CstyleBehaviour()
        this.foldingRules = new FoldMode()
        this.indentKeywords = this.foldingRules.indentKeywords
      }
      var RubyMode = ace.require('ace/mode/ruby').Mode
      oop.inherits(Mode, RubyMode)
      ;(function () {
        this.$id = 'ace/mode/openc3'
      }).call(Mode.prototype)
      return Mode
    },
    submit: function () {
      if (this.existingPluginTxt === null) {
        this.emitSubmit(this.editor.getValue().split('\n'))
      } else {
        // Existing plugin.txt with all diffs resolved
        if (this.differ.diffs.length === 0) {
          this.emitSubmit(this.differ.getEditors().left.getValue().split('\n'))
        } else {
          // Existing plugin.txt with unresolved diffs
          this.$dialog
            .confirm(
              `Diffs still detected! Install using 'Existing plugin.txt' (left) and ignore additional changes in the new plugin.txt (right)?`,
              {
                okText: 'Install',
                cancelText: 'Cancel',
              },
            )
            .then((dialog) => {
              this.emitSubmit(
                this.differ.getEditors().left.getValue().split('\n'),
              )
            })
            .catch((error) => {
              // Cancelled, do nothing
            })
        }
      }
    },
    emitSubmit(lines) {
      let pluginHash = {
        name: this.pluginName,
        variables: this.localVariables,
        plugin_txt_lines: lines,
      }
      this.$emit('submit', pluginHash)
    },
    close: function () {
      this.show = !this.show
    },
  },
}
</script>

<style scoped>
.editor {
  height: 50vh;
  position: relative;
  font-size: 16px;
}

.notice {
  font-size: 20px;
  margin: 10px;
}
.tab {
  background-color: var(--color-background-surface-default);
}
.v-textarea :deep(textarea) {
  padding: 5px;
}
</style>