Source: management-system/src/frontend/views/Capabilities.vue

<template>
  <div>
    <v-toolbar>
      <v-toolbar-title>Capabilities</v-toolbar-title>
      <v-spacer />
      <v-btn color="primary" @click="addCapabilityDialog = true">Add</v-btn>
    </v-toolbar>
    <AlertWindow :popupData="popupData" />

    <confirmation
      v-if="tobeDeletedCapability.all == true"
      :title="'delete all capabilities?'"
      :text="confirmationText"
      continueButtonText="Delete all"
      continueButtonColor="error"
      :show="removeCapabilityDialog"
      maxWidth="350px"
      @cancel="removeCapabilityDialog = false"
      @continue="deleteAllCapabilities"
    />
    <confirmation
      v-else
      :title="'delete the selected capability?'"
      :text="confirmationText"
      continueButtonText="Delete"
      continueButtonColor="error"
      :show="removeCapabilityDialog"
      maxWidth="350px"
      @cancel="removeCapabilityDialog = false"
      @continue="deleteCapability"
    />

    <v-dialog width="600px" eager v-model="parameterInformation">
      <InfoCard
        :title="'Detailed Information about the Parameter'"
        :information="clickedParameter"
        :filter="['name', 'schema', 'type', 'required', 'unit', 'default', 'subTypes']"
        @clicked-object="
          (object) => {
            clickedParameter = object;
          }
        "
        @cancel="parameterInformation = false"
      />
    </v-dialog>

    <v-dialog width="600px" eager v-model="machineInformation">
      <InfoCard
        :title="'Detailed Information about the Machine'"
        :information="clickedMachine"
        :filter="['name', 'ip', 'optionalName', 'id', 'description']"
        :map="{
          name: 'mDNS Name',
          ip: 'IP Address',
          optionalName: 'Name',
          id: 'ID',
          description: 'Description',
        }"
        @cancel="machineInformation = false"
      />
    </v-dialog>

    <!-- Create two Tables: one for all discovered capabilities
    (have one or more associated machines) and one for saved capabilities
    Capabilities are Grouped in Objects:
    { machineId: '...', capabilities: [...]}
    machineId is the id of the associated machine or '' if !mapByHost or there is no machine-->
    <v-container
      fluid
      v-for="(groupArray, index) in getSortedCapabilityGroups()"
      :key="'Container' + index"
    >
      <v-row justify="center" id="wrapper">
        <v-col class="text-center centered">
          <v-card>
            <v-subheader>{{ !index ? 'Discovered' : 'Saved' }}</v-subheader>
            <CapabilityForm
              :show="addCapabilityDialog"
              @cancel="addCapabilityDialog = false"
              @add="addCapability"
            />
            <CapabilityForm
              :show="!!editCapabilityId"
              :capability="editingCapability"
              @cancel="editCapabilityId = ''"
              @update="updateCapability"
            />

            <v-card-text>
              <v-switch
                v-if="index === 0"
                v-model="mapByHost"
                :label="`Group by Machine`"
                style="float: left; margin-top: 0"
              />
              <div class="d-flex justify-end align-center">
                <v-btn
                  :disabled="!elementInDisplayDetailed(groupToCapabilities(groupArray))"
                  small
                  @click="collapseAll(groupToCapabilities(groupArray))"
                >
                  Collapse All
                </v-btn>
                <v-btn
                  :disabled="!notAllInDisplayDetailed(groupToCapabilities(groupArray))"
                  small
                  @click="expandAll(groupToCapabilities(groupArray))"
                >
                  Expand All
                </v-btn>
              </div>
              <v-data-table
                :headers="fields"
                :items="
                  groupArray.length === 1 && groupArray[0].capabilities.length === 0
                    ? []
                    : groupArray
                "
                hide-default-footer
                style="clear: both"
              >
                <template v-slot:header="{ headers }">
                  <tr>
                    <th v-for="header in headers" :key="header.text">
                      <span
                        v-if="header.text === 'Capability'"
                        @click="
                          index === 0
                            ? (sortDiscovered = (sortDiscovered + 1) % 3)
                            : (sortSaved = (sortSaved + 1) % 3)
                        "
                      >
                        <v-icon
                          v-if="
                            (sortDiscovered === 0 && index === 0) || (sortSaved === 0 && index == 1)
                          "
                          small
                        >
                          mdi-arrow-up
                        </v-icon>
                        <v-icon
                          color="primary"
                          v-if="
                            (sortDiscovered === 1 && index === 0) ||
                            (sortSaved === 1 && index === 1)
                          "
                          small
                        >
                          mdi-arrow-down
                        </v-icon>
                        <v-icon
                          color="primary"
                          v-if="
                            (sortDiscovered === 2 && index === 0) ||
                            (sortSaved === 2 && index === 1)
                          "
                          small
                        >
                          mdi-arrow-up
                        </v-icon>
                        {{ header.text }}
                      </span>
                      <span v-else>{{ header.text }}</span>
                    </th>
                  </tr>
                </template>

                <template v-slot:item="{ item, headers }">
                  <tr style="text-align: center" v-if="item.machineId">
                    <td
                      :colspan="headers.length"
                      class="text-left"
                      style="background-color: rgb(196, 216, 245)"
                    >
                      <span class="font-weight-medium">Machine: </span>
                      <a
                        @click.prevent="
                          machineInformation = true;
                          clickedMachine = machinesByIds(item.machineId)[0];
                        "
                      >
                        {{
                          machinesByIds(item.machineId)[0].optionalName ||
                          machinesByIds(item.machineId)[0].name
                        }}
                      </a>
                    </td>
                  </tr>
                  <template v-for="(capability, index) in item.capabilities">
                    <tr
                      style="text-align: center"
                      @click="toggleDisplayDetailed(capability)"
                      :class="elementInDisplayDetailed([capability]) ? 'detailed detailedTop' : ''"
                      :key="item.machineId + 'overview' + index"
                    >
                      <td>{{ capability.name }}</td>
                      <td>
                        <span
                          v-for="(parameter, index) in capability.parameters"
                          :key="parameter + index"
                        >
                          <span :style="{ fontStyle: parameter.required ? 'normal' : 'italic' }">
                            {{ parameter.name }}
                            <span v-if="index != capability.parameters.length - 1">, </span>
                          </span>
                        </span>
                      </td>
                      <td>
                        <div>
                          <v-icon
                            class="mx-0"
                            @click.stop="editCapabilityId = capability.id"
                            color="primary"
                          >
                            mdi-pencil
                          </v-icon>
                          <v-icon
                            class="mx-0"
                            @click.stop="openDeleteCapabilityDialog(false, capability.id)"
                            color="error"
                          >
                            mdi-delete
                          </v-icon>
                        </div>
                      </td>
                    </tr>
                    <!--An additional row after each capability
                    containing detailed information about it, originally hidden-->
                    <tr :key="item.machineId + capability.name + 'Details'">
                      <td
                        :colspan="fields.length"
                        v-if="elementInDisplayDetailed([capability])"
                        class="detailed detailedBottom"
                      >
                        <CapabilityInfo
                          :capability="capability"
                          @machineClick="
                            machineInformation = true;
                            clickedMachine = $event;
                          "
                          @parameterClick="
                            parameterInformation = true;
                            clickedParameter = $event;
                          "
                          @openpopup="openpopup"
                        />
                      </td>
                    </tr>
                  </template>
                </template>
              </v-data-table>
              <div class="mt-3">* Optional parameters are displayed in <i>italic</i></div>
            </v-card-text>
            <v-btn
              v-if="index == 1"
              class="mb-3"
              color="error"
              small
              @click="openDeleteCapabilityDialog(true, 0, index)"
            >
              Delete All
            </v-btn>
          </v-card>
        </v-col>
      </v-row>
    </v-container>
  </div>
</template>

<script>
import * as R from 'ramda';
import uuid from 'uuid';
import { mapState } from 'vuex';
import CapabilityForm from '@/frontend/components/capabilities/CapabilityForm.vue';
import AlertWindow from '@/frontend/components/universal/Alert.vue';
import confirmation from '@/frontend/components/universal/Confirmation.vue';
import InfoCard from '@/frontend/components/capabilities/InformationCard.vue';
import CapabilityInfo from '@/frontend/components/capabilities/CapabilityInfo.vue';

/**
 * @module views
 */
/**
 * @memberof module:views
 * @module Vue:Capabilities
 *
 * @vue-computed machinelessCapabilities
 * @vue-computed capabilitiesWithMachine
 */
export default {
  components: {
    CapabilityForm,
    AlertWindow,
    InfoCard,
    CapabilityInfo,
    confirmation,
  },

  computed: {
    ...mapState({
      capabilities: (state) => state.capabilityStore.capabilities,
      editingCapability() {
        return R.find(R.propEq('id', this.editCapabilityId), this.capabilities);
      },
    }),
    machinelessCapabilities() {
      return this.$store.getters['capabilityStore/machinelessCapabilities'];
    },
    capabilitiesWithMachine() {
      return this.$store.getters['capabilityStore/capabilitiesWithMachine'];
    },
  },
  data() {
    return {
      addCapabilityDialog: false,
      /** */
      removeCapabilityDialog: false,
      /** */
      tobeDeletedCapability: {},
      /** */
      confirmationText: 'Do you want to continue?',
      /** 0: dont sort, 1: sort alphabetical (ascending), 2: sort alphabetical(descending)
       * @type {number}
       */
      sortDiscovered: 1,
      /** */
      sortSaved: 1,
      /** */
      editCapabilityId: '',
      /** */
      mapByHost: false,
      /** */
      displayDetailed: [],
      /** */
      machineInformation: false,
      /** */
      clickedMachine: {},
      /** */
      parameterInformation: false,
      /** */
      clickedParameter: {},
      /** */
      fields: [
        {
          value: 'name',
          text: 'Capability',
          align: 'center',
          sortable: true,
        },
        {
          value: 'parameters',
          text: 'Parameter*',
          align: 'center',
          sortable: false,
        },
        {
          text: '',
          align: 'center',
          sortable: false,
        },
      ],
      /** */
      popupData: {
        body: 'This is not a valid URL',
        display: 'none',
        color: 'error',
      },
    };
  },
  methods: {
    /**
     * returns an array of machines which have one of the given machineIds
     * @param ids an array of machineIds or a signle machineId
     */
    machinesByIds(ids) {
      if (!Array.isArray(ids)) {
        return [this.$store.getters['machineStore/machineById'](ids)];
      }
      return this.$store.getters['machineStore/machines'].filter((machine) =>
        ids.some((id) => machine.id === id)
      );
    },
    /** */
    addCapability(capability) {
      this.$store.dispatch('capabilityStore/add', {
        capability: { ...capability, id: uuid.v4() },
      });
      this.addCapabilityDialog = false;
    },
    /** */
    updateCapability(capability) {
      this.$store.dispatch('capabilityStore/update', {
        capability,
      });
      this.editCapabilityId = '';
    },
    /** */
    openDeleteCapabilityDialog(all, id, index) {
      this.tobeDeletedCapability.id = id;
      this.tobeDeletedCapability.index = index;
      this.tobeDeletedCapability.all = all;
      this.removeCapabilityDialog = true;
    },
    /** */
    deleteCapability() {
      const { id } = this.tobeDeletedCapability;
      this.$store.dispatch('capabilityStore/remove', { id });
      this.removeCapabilityDialog = false;
      this.tobeDeletedCapability = {};
    },
    /** */
    openpopup() {
      this.popupData.display = 'block';
    },
    /**
     * Delete all added capabilities from the store
     */
    deleteAllCapabilities() {
      const { index } = this.tobeDeletedCapability;
      // index 0: remove all capabilities with associated machines, 1: remove all capabilities without associated machines
      if (index === 0) {
        this.capabilitiesWithMachine
          .map((capability) => capability.id)
          .forEach((id) => {
            this.$store.dispatch('capabilityStore/remove', { id });
          });
      } else {
        this.machinelessCapabilities
          .map((capability) => capability.id)
          .forEach((id) => {
            this.$store.dispatch('capabilityStore/remove', { id });
          });
      }
      this.removeCapabilityDialog = false;
      this.tobeDeletedCapability = {};
    },
    /**
     * Show/hide detailed information about the clicked capability
     * by saving/removing the capability from an array where we
     * store the capabilities which have their detailed info visible
     *
     * @param capability - The capability item we want to show additional info about
     */
    toggleDisplayDetailed(capability) {
      if (this.displayDetailed.some((value) => value === capability.id)) {
        this.displayDetailed = this.displayDetailed.filter((value) => value !== capability.id);
      } else {
        this.displayDetailed.push(capability.id);
      }
    },
    /**
     * Hide detailed info about all capabilities of the given array
     * @param capabilities - array of capabilities whos ids are to be removed from displayDetailed
     */
    collapseAll(capabilities) {
      this.displayDetailed = this.displayDetailed.filter((el) =>
        capabilities.every((capability) => capability.id !== el)
      );
    },
    /**
     * Show detailed information about all capabilities
     * @param capabilities - array of capabilities whos id is to be added to displayDetailed
     */
    expandAll(capabilities) {
      capabilities.forEach((capability) => {
        if (!this.displayDetailed.includes(capability.id)) {
          this.displayDetailed.push(capability.id);
        }
      });
    },
    /**
     *  Splits the map returned by the capabilityStore getter map into two arrays and sorts if necessary: 1. noMachine, 2. map without noMachine
     *  noMachine: all Capabilities that are not associated with a machine
     */
    getSortedCapabilityGroups() {
      const map = this.$store.getters['capabilityStore/map'](this.mapByHost);
      const unMapped = map.find((el) => el.machineId === '');

      const noMachine = unMapped.capabilities.filter((el) => !el.machineIds.length);

      unMapped.capabilities = unMapped.capabilities.filter((el) => !noMachine.includes(el));
      if (this.sortDiscovered !== 0) {
        map.forEach((el) => {
          el.capabilities.sort((capA, capB) => {
            let comp = 0;
            const nameA = capA.name.split('/').pop();
            const nameB = capB.name.split('/').pop();
            if (nameA.toLowerCase() < nameB.toLowerCase()) {
              comp = -1;
            } else if (nameA.toLowerCase() > nameB.toLowerCase()) {
              comp = 1;
            }

            if (this.sortDiscovered === 2) {
              comp *= -1;
            }
            return comp;
          });
        });
      }

      if (this.sortSaved !== 0) {
        noMachine.sort((capA, capB) => {
          let comp = 0;
          const nameA = capA.name.split('/').pop();
          const nameB = capB.name.split('/').pop();
          if (nameA.toLowerCase() < nameB.toLowerCase()) {
            comp = -1;
          } else if (nameA.toLowerCase() > nameB.toLowerCase()) {
            comp = 1;
          }

          if (this.sortSaved === 2) {
            comp *= -1;
          }
          return comp;
        });
      }

      return [map, [{ machineId: '', capabilities: noMachine }]];
    },
    /**
     * returns an array containing all capabilities inside the
     * groupArray (can contain duplicates)
     * [test](https://www.google.de) und `[{ machineId: 'id of machine or ""', capabilities: [...]},...]`
     *
     * @param { Array<{machineId: string, capabilities: Array }> } groupArray
     */
    groupToCapabilities(groupArray) {
      return groupArray.map((el) => el.capabilities).flat();
    },
    /** */
    elementInDisplayDetailed(capabilities) {
      return capabilities.some((el) => this.displayDetailed.includes(el.id));
    },
    /**
     *
     */
    notAllInDisplayDetailed(capabilities) {
      return capabilities.some((el) => !this.displayDetailed.includes(el.id));
    },
  },
};
</script>

<style lang="scss">
/* https://sass-lang.com/documentation/syntax#scss */

.v-table__overflow {
  width: 98%;
  overflow-x: auto;
  overflow-y: hidden;
}

.detailed {
  background-color: #f0f0f0;
  border-style: solid;
  border-color: lightgrey;
  border-left-width: 2px;
  border-right-width: 2px;
}

.detailedBottom {
  border-top-width: 0px;
  border-bottom-width: 2px;
}

.detailedTop {
  border-top-width: 2px;
  border-bottom-width: 0px !important;
}
</style>