openc3-cosmos-init/plugins/packages/openc3-tool-common/src/components/CriticalCmdDialog.vue
<!--
# Copyright 2024 OpenC3, Inc.
# 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.
#
# This file may also be used under the terms of a commercial license
# if purchased from OpenC3, Inc.
-->
<template>
<v-dialog :persistent="persistent" v-model="show" width="600">
<v-card>
<v-toolbar height="24">
<v-spacer />
<span> Waiting for Critical Command Approval </span>
<v-spacer />
</v-toolbar>
<v-card-text>
<v-container fluid>
<v-alert type="error" v-model="error" dismissible>
{{ errorText }}
</v-alert>
<v-row class="pt-4" style="color: white"
>User {{ cmdUser }} is waiting for approval to execute:
</v-row>
<v-row class="pa-4" style="color: white">{{ cmdString }}</v-row>
<v-row class="text-subtitle-2">UUID: {{ uuid }}</v-row>
<v-row v-if="!canApprove">
<v-text-field
v-model="username"
label="Username"
data-test="username"
/>
</v-row>
<v-row v-if="!canApprove">
<v-text-field
v-model="password"
type="password"
label="Password"
data-test="password"
/>
</v-row>
<v-row class="pt-2">
<v-spacer />
<v-btn
type="submit"
@click.prevent="reject"
class="mx-2"
color="secondary"
outlined
:disabled="disableButtons"
data-test="reject"
>
Reject
</v-btn>
<v-btn
type="submit"
@click.prevent="approve"
class="mx-2"
color="primary"
:disabled="disableButtons"
data-test="approve"
>
Approve
</v-btn>
</v-row>
</v-container>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
import axios from 'axios'
import Api from '../services/api.js'
export default {
props: {
uuid: String,
cmdUser: String,
cmdString: String,
persistent: {
type: Boolean,
default: false,
},
value: Boolean, // value is the default prop when using v-model
},
data() {
return {
updater: null,
username: null,
password: null,
canApprove: false,
error: false,
errorText: '',
}
},
computed: {
show: {
get() {
return this.value
},
set(value) {
this.$emit('input', value) // input is the default event when using v-model
},
},
formValid: function () {
return !!this.password && !!this.username
},
disableButtons: function () {
return !this.formValid && !this.canApprove
},
},
beforeDestroy() {
clearInterval(this.updater)
this.updater = null
this.canApprove = false
this.username = null
this.password = null
},
watch: {
// Create a watcher on value which is the indicator to display the dialog
// If value is true we request the details from the server
// If this is a tlm dialog we setup an interval to get the telemetry values
value: function (newValue, oldValue) {
if (newValue) {
// Check if we can approve without username/password
Api.get('/openc3-api/criticalcmd/canapprove/' + this.uuid).then(
(response) => {
if (response.data.status === 'ok') {
this.canApprove = true
}
},
)
// Clear if approved/rejected
this.updater = setInterval(() => {
if (this.uuid) {
Api.get('/openc3-api/criticalcmd/status/' + this.uuid).then(
(response) => {
if (response.data.status !== 'WAITING') {
this.$emit('status', response.data.status)
this.show = false
}
},
)
} else {
this.show = false
}
}, 1000)
} else {
clearInterval(this.updater)
this.updater = null
this.canApprove = false
this.username = null
this.password = null
}
},
},
methods: {
async approve() {
if (this.canApprove) {
await Api.post('/openc3-api/criticalcmd/approve/' + this.uuid, {}).then(
(response) => {
if (response.status == 200) {
this.$emit('status', 'APPROVED')
this.show = false
this.error = false
}
},
)
} else {
let token = await this.getKeycloakToken()
axios
.post(
'/openc3-api/criticalcmd/approve/' + this.uuid,
{},
{
headers: {
Authorization: token,
'Content-Type': 'application/json',
},
params: {
scope: window.openc3Scope,
},
timeout: 5000,
},
)
.then((_response) => {
this.$emit('status', 'APPROVED')
this.show = false
this.error = false
})
.catch((error) => {
this.error = true
if (error.response) {
this.errorText = error.response.data.message
} else {
this.errorText = error.message
}
})
}
},
async reject() {
if (this.canApprove) {
await Api.post('/openc3-api/criticalcmd/reject/' + this.uuid, {}).then(
(response) => {
if (response.status == 200) {
this.$emit('status', 'REJECTED')
this.show = false
this.error = false
}
},
)
} else {
let token = await this.getKeycloakToken()
const response = await axios.post(
'/openc3-api/criticalcmd/reject/' + this.uuid,
{},
{
headers: {
Authorization: token,
'Content-Type': 'application/json',
},
params: {
scope: window.openc3Scope,
},
timeout: 5000,
},
)
if (response.status == 200) {
this.$emit('status', 'REJECTED')
this.show = false
this.error = false
}
}
},
async getKeycloakToken() {
const response = await axios.post(
localStorage.keycloakUrl +
'/realms/' +
localStorage.keycloakRealm +
'/protocol/openid-connect/token',
{
username: this.username,
password: this.password,
client_id: localStorage.keycloakClientId,
grant_type: 'password',
scope: 'openid',
},
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
timeout: 5000,
},
)
return response.data.access_token
},
},
}
</script>
<style scoped>
.label {
font-weight: bold;
text-transform: capitalize;
}
:deep(.v-input--selection-controls) {
padding: 0px;
margin: 0px;
}
</style>