openc3-cosmos-init/plugins/packages/openc3-tool-common/src/components/TargetPacketItemChooser.vue
<!--
# 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>
<!-- tgt-pkt-item-chooser class used by Graph.vue to size the graph -->
<div class="pt-4 tgt-pkt-item-chooser">
<v-row>
<v-col :cols="colSize" class="select" data-test="select-target">
<v-autocomplete
label="Select Target"
hide-details
dense
outlined
@change="targetNameChanged"
:items="targetNames"
item-text="label"
item-value="value"
v-model="selectedTargetName"
/>
</v-col>
<v-col :cols="colSize" class="select" data-test="select-packet">
<v-autocomplete
label="Select Packet"
hide-details
dense
outlined
@change="packetNameChanged"
:disabled="packetsDisabled || autocompleteDisabled"
:items="packetNames"
item-text="label"
item-value="value"
v-model="selectedPacketName"
/>
</v-col>
<v-col
v-if="chooseItem"
:cols="colSize"
class="select"
data-test="select-item"
>
<v-autocomplete
label="Select Item"
hide-details
dense
outlined
@change="itemNameChanged($event)"
:disabled="itemsDisabled || autocompleteDisabled"
:items="itemNames"
item-text="label"
item-value="value"
v-model="selectedItemName"
/>
</v-col>
<v-col
v-if="chooseItem && itemIsArray()"
cols="1"
class="select"
data-test="array-index"
>
<v-combobox
label="Index"
hide-details
dense
outlined
@change="indexChanged($event)"
:disabled="itemsDisabled || autocompleteDisabled"
:items="arrayIndexes()"
item-text="label"
item-value="value"
v-model="selectedArrayIndex"
/>
</v-col>
<v-col v-if="buttonText" :cols="colSize" style="max-width: 140px">
<v-btn
:disabled="buttonDisabled"
block
color="primary"
data-test="select-send"
@click="buttonPressed"
>
{{ actualButtonText }}
</v-btn>
</v-col>
</v-row>
<v-row v-if="selectTypes">
<v-col :cols="colSize" class="select" data-test="data-type">
<v-autocomplete
label="Value Type"
hide-details
dense
outlined
:items="valueTypes"
v-model="selectedValueType"
/>
</v-col>
<v-col :cols="colSize" class="select" data-test="reduced">
<v-autocomplete
label="Reduced"
hide-details
dense
outlined
:items="reductionModes"
v-model="selectedReduced"
/>
</v-col>
<v-col :cols="colSize" class="select" data-test="reduced-type">
<v-autocomplete
label="Reduced Type"
hide-details
dense
outlined
:disabled="selectedReduced === 'DECOM'"
:items="reducedTypes"
v-model="selectedReducedType"
/>
</v-col>
<v-col :cols="colSize" style="max-width: 140px"> </v-col>
</v-row>
<v-row no-gutters class="pt-1">
<v-col v-if="hazardous" :cols="colSize" class="openc3-yellow"
>Description: {{ description }} (HAZARDOUS)</v-col
>
<v-col v-else :cols="colSize">Description: {{ description }} </v-col>
</v-row>
</div>
</template>
<script>
import { OpenC3Api } from '../services/openc3-api'
export default {
props: {
allowAll: {
type: Boolean,
default: false,
},
allowAllTargets: {
type: Boolean,
default: false,
},
buttonText: {
type: String,
default: null,
},
chooseItem: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
initialTargetName: {
type: String,
default: '',
},
initialPacketName: {
type: String,
default: '',
},
initialItemName: {
type: String,
default: '',
},
selectTypes: {
type: Boolean,
default: false,
},
mode: {
type: String,
default: 'tlm',
// TODO: add validators throughout
validator: (propValue) => {
return ['cmd', 'tlm'].includes(propValue)
},
},
unknown: {
type: Boolean,
default: false,
},
vertical: {
type: Boolean,
default: false,
},
hidden: {
type: Boolean,
default: false,
},
},
data() {
return {
targetNames: [],
selectedTargetName: this.initialTargetName?.toUpperCase(),
packetNames: [],
selectedPacketName: this.initialPacketName?.toUpperCase(),
itemNames: [],
selectedItemName: this.initialItemName?.toUpperCase(),
valueTypes: ['CONVERTED', 'RAW'],
selectedValueType: 'CONVERTED',
reductionModes: [
// Map NONE to DECOM for clarity
{ text: 'NONE', value: 'DECOM' },
{ text: 'REDUCED_MINUTE', value: 'REDUCED_MINUTE' },
{ text: 'REDUCED_HOUR', value: 'REDUCED_HOUR' },
{ text: 'REDUCED_DAY', value: 'REDUCED_DAY' },
],
selectedArrayIndex: null,
selectedReduced: 'DECOM',
reducedTypes: ['MIN', 'MAX', 'AVG', 'STDDEV'],
selectedReducedType: 'AVG',
description: '',
hazardous: false,
internalDisabled: false,
packetsDisabled: false,
itemsDisabled: false,
api: null,
ALL: {
label: '[ ALL ]',
value: 'ALL',
description: 'ALL',
}, // Constant to indicate all packets or items
UNKNOWN: {
label: '[ UNKNOWN ]',
value: 'UNKNOWN',
description: 'UNKNOWN',
},
}
},
created() {
this.internalDisabled = true
this.api = new OpenC3Api()
this.api.get_target_names().then((result) => {
this.targetNames = result.flatMap((target) => {
// Ignore the UNKNOWN target as it doesn't make sense to select this
if (target == 'UNKNOWN') {
return []
}
return { label: target, value: target }
})
// TODO: This is a nice enhancement but results in logs of API calls for many targets
// See if we can reduce this to a single API call
// Filter out any targets without packets
// for (var i = this.targetNames.length - 1; i >= 0; i--) {
// const cmd =
// this.mode === 'tlm' ? 'get_all_tlm_names' : 'get_all_cmd_names'
// await this.api[cmd](this.targetNames[i].value).then((names) => {
// if (names.length === 0) {
// this.targetNames.splice(i, 1)
// }
// })
// }
if (this.allowAllTargets) {
this.targetNames.unshift(this.ALL)
}
// If the initial target name is not set, default to the first target
// which also updates packets and items as needed
if (!this.selectedTargetName) {
this.selectedTargetName = this.targetNames[0].value
this.targetNameChanged(this.selectedTargetName)
} else {
// Selected target name was set but we still have to update packets
this.updatePackets()
}
if (this.unknown) {
this.targetNames.push(this.UNKNOWN)
}
})
},
computed: {
actualButtonText: function () {
if (this.selectedPacketName === 'ALL') {
return 'Add Target'
}
if (this.selectedItemName === 'ALL') {
return 'Add Packet'
}
return this.buttonText
},
autocompleteDisabled: function () {
return this.disabled || this.internalDisabled
},
buttonDisabled: function () {
return (
this.disabled ||
this.internalDisabled ||
this.selectedTargetName === null ||
this.selectedPacketName === null ||
this.selectedItemNameWIndex === null
)
},
colSize: function () {
return this.vertical ? 12 : false
},
selectedItemNameWIndex: function () {
if (
this.selectedArrayIndex !== null &&
this.selectedArrayIndex !== this.ALL.label
) {
return `${this.selectedItemName}[${this.selectedArrayIndex}]`
} else {
return this.selectedItemName
}
},
},
watch: {
mode: function (newVal, oldVal) {
this.selectedPacketName = null
this.selectedItemName = null
// This also updates packets and items as needed
this.targetNameChanged(this.selectedTargetName)
},
chooseItem: function (newVal, oldVal) {
if (newVal) {
this.updateItems()
} else {
this.itemNames = []
}
},
},
methods: {
updatePackets: function () {
if (this.selectedTargetName === 'UNKNOWN') {
this.packetNames = [this.UNKNOWN]
this.selectedPacketName = this.packetNames[0].value
this.updatePacketDetails(this.UNKNOWN.value)
this.description = 'UNKNOWN'
return
}
if (this.selectedTargetName === 'ALL') {
this.packetNames = [this.ALL]
this.selectedPacketName = this.packetNames[0].value
this.updatePacketDetails(this.ALL.value)
this.description = 'ALL'
return
}
this.internalDisabled = true
const cmd =
this.mode === 'tlm' ? 'get_all_tlm_names' : 'get_all_cmd_names'
this.api[cmd](this.selectedTargetName, this.hidden).then((names) => {
this.packetNames = names.map((name) => {
return {
label: name,
value: name,
}
})
if (this.allowAll) {
this.packetNames.unshift(this.ALL)
}
if (!this.selectedPacketName) {
this.selectedPacketName = this.packetNames[0].value
}
this.updatePacketDetails(this.selectedPacketName)
const item = this.packetNames.find((packet) => {
return packet.value === this.selectedPacketName
})
if (item && this.chooseItem) {
this.updateItems()
}
this.internalDisabled = false
})
},
updateItems: function () {
if (this.selectedPacketName === 'ALL') {
return
}
this.internalDisabled = true
const cmd = this.mode === 'tlm' ? 'get_tlm' : 'get_cmd'
this.api[cmd](this.selectedTargetName, this.selectedPacketName).then(
(packet) => {
this.itemNames = packet.items
.map((item) => {
let label = item.name
if (item.data_type == 'DERIVED') {
label += ' *'
}
return [
{
label: label,
value: item.name,
description: item.description,
array: item.array_size / item.bit_size,
},
]
})
.reduce((result, item) => {
return result.concat(item)
}, [])
this.itemNames.sort((a, b) => (a.label > b.label ? 1 : -1))
if (this.allowAll) {
this.itemNames.unshift(this.ALL)
}
if (!this.selectedItemName) {
this.selectedItemName = this.itemNames[0].value
}
this.description = this.itemNames[0].description
this.itemIsArray()
this.$emit('on-set', {
targetName: this.selectedTargetName,
packetName: this.selectedPacketName,
itemName: this.selectedItemNameWIndex,
valueType: this.selectedValueType,
reduced: this.selectedReduced,
reducedType: this.selectedReducedType,
})
this.internalDisabled = false
},
)
},
itemIsArray: function () {
let i = this.itemNames.findIndex(
(item) => item.value === this.selectedItemName,
)
if (i === -1) {
this.selectedArrayIndex = null
return false
}
if (isNaN(this.itemNames[i].array)) {
this.selectedArrayIndex = null
return false
} else {
if (this.selectedArrayIndex === null) {
this.selectedArrayIndex = 0
}
return true
}
},
arrayIndexes: function () {
let i = this.itemNames.findIndex(
(item) => item.value === this.selectedItemName,
)
let indexes = [...Array(this.itemNames[i].array).keys()]
if (this.allowAll) {
indexes.unshift(this.ALL.label)
}
return indexes
},
targetNameChanged: function (value) {
this.selectedTargetName = value
this.selectedPacketName = ''
this.selectedItemName = ''
// When the target name is completed deleted in the v-autocomplete
// the @change handler is fired but the value is null
// In this case we don't want to update packets
if (value !== null) {
this.updatePackets()
}
},
packetNameChanged: function (value) {
this.selectedItemName = ''
// When the packet name is completed deleted in the v-autocomplete
// the @change handler is fired but the value is null
// In this case we don't want to update packet details
if (value !== null) {
this.updatePacketDetails(value)
}
},
updatePacketDetails: function (value) {
if (value === 'ALL') {
this.itemsDisabled = true
this.internalDisabled = false
} else {
this.itemsDisabled = false
const packet = this.packetNames.find((packet) => {
return value === packet.value
})
if (packet) {
this.selectedPacketName = packet.value
const cmd = this.mode === 'tlm' ? 'get_tlm' : 'get_cmd'
this.api[cmd](this.selectedTargetName, this.selectedPacketName).then(
(packet) => {
this.description = packet.description
this.hazardous = packet.hazardous
},
)
}
}
if (this.chooseItem) {
this.updateItems()
} else {
this.$emit('on-set', {
targetName: this.selectedTargetName,
packetName: this.selectedPacketName,
itemName: this.selectedItemNameWIndex,
valueType: this.selectedValueType,
reduced: this.selectedReduced,
reducedType: this.selectedReducedType,
})
}
},
itemNameChanged: function (value) {
const item = this.itemNames.find((item) => {
return value === item.value
})
if (item) {
this.itemIsArray()
this.selectedItemName = item.value
this.description = item.description
this.$emit('on-set', {
targetName: this.selectedTargetName,
packetName: this.selectedPacketName,
itemName: this.selectedItemNameWIndex,
valueType: this.selectedValueType,
reduced: this.selectedReduced,
reducedType: this.selectedReducedType,
})
}
},
indexChanged: function (value) {
this.$emit('on-set', {
targetName: this.selectedTargetName,
packetName: this.selectedPacketName,
itemName: this.selectedItemNameWIndex,
valueType: this.selectedValueType,
reduced: this.selectedReduced,
reducedType: this.selectedReducedType,
})
},
buttonPressed: function () {
if (this.selectedPacketName === 'ALL') {
this.allTargetPacketItems()
} else if (this.selectedItemName === 'ALL') {
this.allPacketItems()
} else if (this.chooseItem) {
this.$emit('click', {
targetName: this.selectedTargetName,
packetName: this.selectedPacketName,
itemName: this.selectedItemNameWIndex,
valueType: this.selectedValueType,
reduced: this.selectedReduced,
reducedType: this.selectedReducedType,
})
} else {
this.$emit('click', {
targetName: this.selectedTargetName,
packetName: this.selectedPacketName,
valueType: this.selectedValueType,
reduced: this.selectedReduced,
reducedType: this.selectedReducedType,
})
}
},
allTargetPacketItems: function () {
this.packetNames.forEach((packetName) => {
if (packetName === this.ALL) return
const cmd = this.mode === 'tlm' ? 'get_tlm' : 'get_cmd'
this.api[cmd](this.selectedTargetName, packetName.value).then(
(packet) => {
packet.items.forEach((item) => {
this.$emit('click', {
targetName: this.selectedTargetName,
packetName: packetName.value,
itemName: item['name'],
valueType: this.selectedValueType,
reduced: this.selectedReduced,
reducedType: this.selectedReducedType,
})
})
},
)
})
},
allPacketItems: function () {
this.itemNames.forEach((item) => {
if (item === this.ALL) return
this.$emit('click', {
targetName: this.selectedTargetName,
packetName: this.selectedPacketName,
itemName: item.value,
valueType: this.selectedValueType,
reduced: this.selectedReduced,
reducedType: this.selectedReducedType,
})
})
},
},
}
</script>
<style scoped>
.button {
padding: 4px;
}
.select {
max-width: 300px;
}
.row + .row {
margin-top: 0px;
}
</style>