From d54a8d73be72eedeb6d0bdf219c97208abc828ab Mon Sep 17 00:00:00 2001 From: ingalls <nick@ingalls.ca> Date: Thu, 17 Oct 2024 10:41:38 -0600 Subject: [PATCH 1/4] VideoLease --- api/web/package-lock.json | 30 +- .../src/components/CloudTAK/Menu/Videos.vue | 267 +++++++++++------- .../CloudTAK/Menu/Videos/VideoLeaseModal.vue | 216 +++++++------- .../src/components/CloudTAK/util/Video.vue | 8 +- api/web/src/stores/map.ts | 1 + api/web/src/types.ts | 5 +- 6 files changed, 296 insertions(+), 231 deletions(-) diff --git a/api/web/package-lock.json b/api/web/package-lock.json index 37a131d8b..dfaa8ec06 100644 --- a/api/web/package-lock.json +++ b/api/web/package-lock.json @@ -3213,9 +3213,9 @@ ] }, "node_modules/@sinclair/typebox": { - "version": "0.33.16", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.33.16.tgz", - "integrity": "sha512-jsz4f3LxXGh217hJE3MAOB+i3pzgr9wLGUBiCRNWaG/rRcVoS4+dzQok9SeZLtwNdmY44oGYQWlJCjIJxeLKEw==", + "version": "0.33.17", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.33.17.tgz", + "integrity": "sha512-75232GRx3wp3P7NP+yc4nRK3XUAnaQShxTAzapgmQrgs0QvSq0/mOJGoZXRpH15cFCKyys+4laCPbBselqJ5Ag==", "license": "MIT" }, "node_modules/@surma/rollup-plugin-off-main-thread": { @@ -3955,9 +3955,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", - "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "version": "22.7.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.6.tgz", + "integrity": "sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==", "dev": true, "license": "MIT", "dependencies": { @@ -5619,9 +5619,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.39", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.39.tgz", - "integrity": "sha512-4xkpSR6CjuiaNyvwiWDI85N9AxsvbPawB8xc7yzLPonYTuP19BVgYweKyUMFtHEZgIcHWMt1ks5Cqx2m+6/Grg==", + "version": "1.5.40", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.40.tgz", + "integrity": "sha512-LYm78o6if4zTasnYclgQzxEcgMoIcybWOhkATWepN95uwVVWV0/IW10v+2sIeHE+bIYWipLneTftVyQm45UY7g==", "dev": true, "license": "ISC" }, @@ -9168,9 +9168,9 @@ } }, "node_modules/sass": { - "version": "1.79.5", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz", - "integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==", + "version": "1.80.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.80.1.tgz", + "integrity": "sha512-9lBwDZ7j3y/1DKj5Ec249EVGo5CVpwnzIyIj+cqlCjKkApLnzsJ/l9SnV4YnORvW9dQwQN+gQvh/mFZ8CnDs7Q==", "license": "MIT", "dependencies": { "@parcel/watcher": "^2.4.1", @@ -9930,9 +9930,9 @@ } }, "node_modules/terra-draw": { - "version": "1.0.0-beta.6", - "resolved": "https://registry.npmjs.org/terra-draw/-/terra-draw-1.0.0-beta.6.tgz", - "integrity": "sha512-0ICoTJXL4fG3TeHXpZye5zu+PcAkFOliPn2LH+NMJFnCdjY6NGXhsQgXR53W0+Vd8cduZcBVe1VuaBmB6zZN/Q==", + "version": "1.0.0-beta.7", + "resolved": "https://registry.npmjs.org/terra-draw/-/terra-draw-1.0.0-beta.7.tgz", + "integrity": "sha512-k6Z29O3tpfqXDAivzVZUY/ohT6kC0zvrefJb3Dm/DE+yPG7Qp7Dy8dDK+FTFN3MpSpemThRyyXbic/Y6gtnWwQ==", "license": "MIT" }, "node_modules/terser": { diff --git a/api/web/src/components/CloudTAK/Menu/Videos.vue b/api/web/src/components/CloudTAK/Menu/Videos.vue index 0d24e22d4..8889fb66b 100644 --- a/api/web/src/components/CloudTAK/Menu/Videos.vue +++ b/api/web/src/components/CloudTAK/Menu/Videos.vue @@ -4,80 +4,133 @@ :loading='loading' > <template #buttons> - <IconPlus - v-tooltip='"Get Lease"' - :size='32' - :stroke='1' - class='cursor-pointer' + <TablerIconButton + v-if='mode === "lease"' @click='lease={}' - /> - <IconRefresh - v-tooltip='"Refresh"' - :size='32' - :stroke='1' - class='cursor-pointer' + title='Get Lease' + ><IconPlus :size='32' stroke='1'/></TablerIconButton> + <TablerIconButton @click='refresh' - /> + title='Refresh' + ><IconRefresh :size='32' stroke='1'/></TablerIconButton> </template> <template #default> <div - v-for='l in leases.items' - :key='l.id' - class='col-12 py-2 px-3 d-flex align-items-center hover-dark cursor-pointer' - @click='lease = l' + class='px-2 py-2 round btn-group w-100' + role='group' > - <div class='row g-0 w-100'> - <div class='d-flex align-items-center w-100'> - <IconVideo - :size='32' - :stroke='1' - /> - <span - class='mx-2' - v-text='l.name' - /> - - <div class='ms-auto'> - <TablerDelete - displaytype='icon' - @delete='deleteLease(l)' + <input + id='streams' + type='radio' + class='btn-check' + autocomplete='off' + :checked='mode === "streams"' + @click='mode = "streams"' + > + <label + for='streams' + type='button' + class='btn btn-sm' + ><IconVideo + v-tooltip='"Video Streams"' + :size='32' + stroke='1' + /></label> + + <input + id='lease' + type='radio' + class='btn-check' + autocomplete='off' + :checked='mode === "lease"' + @click='mode = "lease"' + > + + <label + for='lease' + type='button' + class='btn btn-sm' + ><IconServer2 + v-tooltip='"Video Leases"' + :size='32' + stroke='1' + /></label> + </div> + + <template v-if='mode === "streams"'> + <div class='col-12'> + <TablerNone + v-if='!cotStore.videos().size' + label='Video Streams' + :create='false' + /> + <template v-else> + <div + v-for='video in cotStore.videos()' + :key='video.id' + class='col-12 py-2 px-3 d-flex align-items-center hover-dark cursor-pointer' + @click='$router.push(`/cot/${video.id}`)' + > + <IconVideo + :size='32' + stroke='1' + /> + <span + class='mx-2' + style='font-size: 18px;' + v-text='video.properties.callsign' /> </div> - </div> - <div class='col-12 my-0 py-0'> - <span - style='margin-left: 42px;' - class='subheader' - v-text='l.expiration' - /> - </div> + </template> </div> - </div> - <div class='col-12'> + </template> + <template v-else-if='mode === "lease"'> <TablerNone - v-if='!videos.size' - label='Video Streams' + v-if='leases.total === 0' + label='Video Leases' :create='false' /> - <template v-else> - <div - v-for='video in videos' - :key='video.id' - class='col-12 py-2 px-3 d-flex align-items-center hover-dark cursor-pointer' - @click='$router.push(`/cot/${video.id}`)' - > - <IconVideo - :size='32' - :stroke='1' - /> - <span - class='mx-2' - style='font-size: 18px;' - v-text='video.properties.callsign' - /> + <div + v-else + v-for='l in leases.items' + :key='l.id' + class='col-12 py-2 px-3 d-flex align-items-center hover-dark cursor-pointer' + @click='lease = l' + > + <div class='row g-0 w-100'> + <div class='d-flex align-items-center w-100'> + <IconVideo + :size='32' + stroke='1' + /> + <span + class='mx-2' + v-text='l.name' + /> + + <div class='ms-auto'> + <TablerDelete + displaytype='icon' + @delete='deleteLease(l)' + /> + </div> + </div> + <div class='col-12 my-0 py-0'> + <span + v-if='expired(l.expiration)' + style='margin-left: 42px;' + class='subheader text-red' + >Expired Lease</span> + <span + v-else + style='margin-left: 42px;' + class='subheader' + v-text='l.expiration' + /> + </div> </div> - </template> - </div> + </div> + </template> </template> </MenuTemplate> @@ -89,67 +142,65 @@ /> </template> -<script> +<script setup lang='ts'> import MenuTemplate from '../util/MenuTemplate.vue'; import VideoLeaseModal from './Videos/VideoLeaseModal.vue'; -import { std } from '/src/std.ts'; -import { useCOTStore } from '/src/stores/cots.ts'; +import { std } from '../../../std.ts'; +import type { VideoLease, VideoLeaseList } from '../../../types.ts'; +import { useCOTStore } from '../../../stores/cots.ts'; import { TablerNone, TablerDelete, + TablerIconButton } from '@tak-ps/vue-tabler'; import { IconPlus, IconVideo, IconRefresh, + IconServer2, } from '@tabler/icons-vue'; +import { ref, onMounted } from 'vue' + const cotStore = useCOTStore(); -export default { - name: 'CloudTAKVideos', - components: { - IconPlus, - IconVideo, - IconRefresh, - MenuTemplate, - VideoLeaseModal, - TablerNone, - TablerDelete, - }, - data: function() { - return { - loading: true, - lease: false, - leases: { - total: 0, - items: [] - }, - videos: cotStore.videos() - } - }, - mounted: async function() { - await this.fetchLeases(); - }, - methods: { - refresh: async function() { - await this.fetchLeases(); - this.videos = cotStore.videos(); - }, - fetchLeases: async function() { - this.lease = false; - this.loading = true; - this.leases = await std('/api/video/lease') - this.loading = false; - }, - deleteLease: async function(lease) { - this.loading = true; - await std(`/api/video/lease/${lease.id}`, { - method: 'DELETE' - }); - - await this.fetchLeases(); - } +const mode = ref('streams'); +const loading = ref(true); +const lease = ref(); +const leases = ref<VideoLeaseList>({ total: 0, items: [] }); + +onMounted(async () => { + await fetchLeases(); +}); + +function expired(expiration: string): boolean { + return +new Date(expiration) < +new Date(); +} + +async function refresh(): Promise<void> { + await fetchLeases(); +} + +async function fetchLeases(): Promise<void> { + lease.value = undefined; + loading.value = true; + leases.value = await std('/api/video/lease') as VideoLeaseList + loading.value = false; +} + +async function deleteLease(lease: VideoLease): Promise<void> { + loading.value = true; + + try { + await std(`/api/video/lease/${lease.id}`, { + method: 'DELETE' + }); + + await fetchLeases(); + } catch (err) { + loading.value = false; + + throw err; } } </script> diff --git a/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue b/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue index 63fa4a4c0..c919d68b6 100644 --- a/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue +++ b/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue @@ -5,7 +5,7 @@ type='button' class='btn-close' aria-label='Close' - @click='$emit("refresh")' + @click='emit("refresh")' /> <div class='modal-header'> <div @@ -22,7 +22,7 @@ <IconRefresh v-if='editLease.id' :size='32' - :stroke='1' + stroke='1' class='cursor-pointer' @click='fetchLease' /> @@ -50,7 +50,10 @@ <div class='subheader pt-4'> RTSP Path </div> - <CopyField :text='protocols.rtsp.url.replace(/.*\//, "")' /> + <CopyField + v-if='protocols.rtsp' + :text='protocols.rtsp.url.replace(/.*\//, "")' + /> </div> </div> </div> @@ -63,7 +66,7 @@ > <IconChevronLeft :size='20' - :stroke='1' + stroke='1' /> <span v-if='wizard === 1' @@ -90,7 +93,7 @@ >Done</span> <IconChevronRight :size='20' - :stroke='1' + stroke='1' /> </button> </div> @@ -123,12 +126,12 @@ <IconSquareChevronRight v-if='!advanced' :size='32' - :stroke='1' + stroke='1' /> <IconChevronDown v-else :size='32' - :stroke='1' + stroke='1' /> Advanced Options </label> @@ -154,13 +157,20 @@ <div class='subheader pt-4'> Video Streaming Protocols </div> - <div - v-for='protocol in protocols' - class='pt-2' - > - <div v-text='protocol.name' /> - <CopyField :text='protocol.url' /> - </div> + <template v-if='expired(lease.expiration)'> + <TablerAlert :err='new Error("Expired Lease")'/> + </template> + <template v-else> + <div + v-for='protocol in protocols' + class='pt-2' + > + <template v-if='protocol'> + <div v-text='protocol.name' /> + <CopyField :text='protocol.url' /> + </template> + </div> + </template> </template> </div> </div> @@ -172,7 +182,7 @@ > <IconWand :size='20' - :stroke='1' + stroke='1' /> <span class='mx-2'>UAS Tool Wizard</span> </button> @@ -187,9 +197,11 @@ </TablerModal> </template> -<script> -import { std } from '/src/std.ts'; +<script setup lang='ts'> +import { std } from '../../../../std.ts'; import CopyField from '../../util/CopyField.vue'; +import { defineProps, defineEmits, ref, onMounted } from 'vue'; +import type { VideoLease, VideoLeaseResponse, VideoLeaseProtocols } from '../../../../types.ts'; import { IconRefresh, IconWand, @@ -200,109 +212,107 @@ import { } from '@tabler/icons-vue'; import { TablerLoading, + TablerAlert, TablerModal, TablerInput, TablerEnum, TablerDelete } from '@tak-ps/vue-tabler' -export default { - name: 'VideoLeaseModal', - components: { - CopyField, - IconWand, - IconRefresh, - IconSquareChevronRight, - IconChevronRight, - IconChevronLeft, - IconChevronDown, - TablerModal, - TablerEnum, - TablerInput, - TablerDelete, - TablerLoading, - }, - props: { - lease: { - type: Object, - required: true - } - }, - emits: [ - 'close', - 'refresh' - ], - data: function() { - return { - loading: true, - wizard: 0, - advanced: false, - protocols: {}, - editLease: { - name: '', - duration: '16 Hours', - stream_user: '', - stream_pass: '' - } - } - }, - mounted: async function() { - if (this.lease.id) { - this.editLease = this.lease; - await this.fetchLease(); +const props = defineProps<{ + lease: VideoLease +}>(); + +const emit = defineEmits([ 'close', 'refresh' ]) + +const loading = ref(true); +const wizard = ref(0); +const advanced = ref(false); +const protocols = ref<VideoLeaseProtocols>({}); +const editLease = ref<{ + id?: number + name: string + duration: string + stream_user: string | null + stream_pass: string | null +}>({ + name: '', + duration: '16 Hours', + stream_user: '', + stream_pass: '' +}); + +onMounted(async () => { + if (props.lease.id) { + editLease.value = { + ...props.lease, + duration: '16 Hours' } + await fetchLease(); + } - this.loading = false - }, - methods: { - fetchLease: async function() { - this.loading = true; + loading.value = false +}); - const { lease, protocols } = await std(`/api/video/lease/${this.editLease.id}`, { - method: 'GET', - }); +function expired(expiration: string) { + return +new Date(expiration) < +new Date(); +} - this.editLease = lease; - this.protocols = protocols; +async function fetchLease() { + loading.value = true; - this.loading = false; - }, - deleteLease: async function() { - await std(`/api/video/lease/${this.lease.id}`, { - method: 'DELETE', - }); + const res = await std(`/api/video/lease/${editLease.value.id}`, { + method: 'GET', + }) as VideoLeaseResponse; - this.$emit('refresh'); - }, - saveLease: async function() { - this.loading = true; + editLease.value = { + ...res.lease, + duration: '16 Hours' + } + protocols.value = res.protocols; + + loading.value = false; +} + +async function deleteLease() { + await std(`/api/video/lease/${props.lease.id}`, { + method: 'DELETE', + }); + + emit('refresh'); +} - if (this.editLease.id) { - await std(`/api/video/lease/${this.editLease.id}`, { - method: 'PATCH', - body: this.editLease - }); - this.$emit('refresh'); - } else { - const { lease } = await std('/api/video/lease', { - method: 'POST', - body: { - name: this.editLease.name, - duration: parseInt(this.editLease.duration.split(' ')[0]) * 60 * 60 - } - }); +async function saveLease() { + loading.value = true; - if (this.editLease.id) { - this.$emit('refresh') - } else { - this.editLease = lease; + if (editLease.value.id) { + await std(`/api/video/lease/${editLease.value.id}`, { + method: 'PATCH', + body: editLease.value + }) as VideoLeaseResponse; - await this.fetchLease(); - } + emit('refresh'); + } else { + const { lease } = await std('/api/video/lease', { + method: 'POST', + body: { + name: editLease.value.name, + duration: parseInt(editLease.value.duration.split(' ')[0]) * 60 * 60 + } + }) as VideoLeaseResponse; - this.loading = false; + if (editLease.value.id) { + emit('refresh') + } else { + editLease.value = { + ...lease, + duration: '16 Hours' } - }, + + await fetchLease(); + } + + loading.value = false; } } </script> diff --git a/api/web/src/components/CloudTAK/util/Video.vue b/api/web/src/components/CloudTAK/util/Video.vue index 690b1238f..e731150ea 100644 --- a/api/web/src/components/CloudTAK/util/Video.vue +++ b/api/web/src/components/CloudTAK/util/Video.vue @@ -43,7 +43,7 @@ <script lang='ts'> import { defineComponent } from 'vue' import { std } from '../../../../src/std.ts'; -import type { VideoLease } from '../../../types.ts'; +import type { VideoLeaseResponse } from '../../../types.ts'; import type Player from 'video.js/dist/types/player.d.ts'; import videojs from 'video.js'; import 'video.js/dist/video-js.css'; @@ -72,9 +72,9 @@ export default defineComponent({ data: function(): { loading: boolean, err?: Error, - lease?: VideoLease["lease"], + lease?: VideoLeaseResponse["lease"], player?: Player, - protocols?: VideoLease["protocols"] + protocols?: VideoLeaseResponse["protocols"] } { return { loading: true @@ -121,7 +121,7 @@ export default defineComponent({ duration: 1 * 60 * 60, proxy: this.video } - }) as VideoLease + }) as VideoLeaseResponse this.lease = lease; this.protocols = protocols; diff --git a/api/web/src/stores/map.ts b/api/web/src/stores/map.ts index ab07d8bb6..8f9821b63 100644 --- a/api/web/src/stores/map.ts +++ b/api/web/src/stores/map.ts @@ -435,6 +435,7 @@ export const useMapStore = defineStore('cloudtak', { initDraw: function() { this.draw = new terraDraw.TerraDraw({ adapter: new terraDraw.TerraDrawMapLibreGLAdapter({ + // @ts-expect-error TODO Figure out why this is failing map: this.map }), idStrategy: { diff --git a/api/web/src/types.ts b/api/web/src/types.ts index 858e5f9c8..c53508a8c 100644 --- a/api/web/src/types.ts +++ b/api/web/src/types.ts @@ -15,7 +15,10 @@ export type APIList<T> = { items: Array<T>; } -export type VideoLease = paths["/video/lease/{:lease}"]["get"]["responses"]["200"]["content"]["application/json"]; +export type VideoLease = paths["/video/lease/{:lease}"]["get"]["responses"]["200"]["content"]["application/json"]["lease"]; +export type VideoLeaseList = paths["/video/lease"]["get"]["responses"]["200"]["content"]["application/json"]; +export type VideoLeaseProtocols = paths["/video/lease/{:lease}"]["get"]["responses"]["200"]["content"]["application/json"]["protocols"]; +export type VideoLeaseResponse = paths["/video/lease/{:lease}"]["get"]["responses"]["200"]["content"]["application/json"] export type Group = paths["/marti/group"]["get"]["responses"]["200"]["content"]["application/json"]["data"][0] From 3b404059eb32f89ee75376c2718677d11b76d6f1 Mon Sep 17 00:00:00 2001 From: ingalls <nick@ingalls.ca> Date: Thu, 17 Oct 2024 10:53:51 -0600 Subject: [PATCH 2/4] Add Lease Renewal --- api/web/package-lock.json | 6 ++-- .../CloudTAK/Menu/Mission/MissionLogs.vue | 2 +- .../CloudTAK/Menu/Videos/VideoLeaseModal.vue | 33 ++++++++++++++----- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/api/web/package-lock.json b/api/web/package-lock.json index dfaa8ec06..21e5e2cfa 100644 --- a/api/web/package-lock.json +++ b/api/web/package-lock.json @@ -3390,9 +3390,9 @@ } }, "node_modules/@tak-ps/vue-tabler": { - "version": "3.55.3", - "resolved": "https://registry.npmjs.org/@tak-ps/vue-tabler/-/vue-tabler-3.55.3.tgz", - "integrity": "sha512-RDXRvfgKNQg0GWg5jBlQDwpAVcLYqjvulQK9HVJTc8O7CS9cc/O2xdE132/woczKcuzfTAh14+Z9OWB3QXK+rw==", + "version": "3.56.0", + "resolved": "https://registry.npmjs.org/@tak-ps/vue-tabler/-/vue-tabler-3.56.0.tgz", + "integrity": "sha512-9bbqX2Ys/9zPbIxhFag+QGCTZbXurYLnFSTrUD3ProEmiD1G92nYcnQy6jqMXcQqGyWG0BYzmxrpZpeggHPk3A==", "license": "ISC", "dependencies": { "@tabler/icons-vue": "^3.0.0", diff --git a/api/web/src/components/CloudTAK/Menu/Mission/MissionLogs.vue b/api/web/src/components/CloudTAK/Menu/Mission/MissionLogs.vue index 5436f6049..876f2ce17 100644 --- a/api/web/src/components/CloudTAK/Menu/Mission/MissionLogs.vue +++ b/api/web/src/components/CloudTAK/Menu/Mission/MissionLogs.vue @@ -103,7 +103,7 @@ </template> <script setup lang='ts'> -import { ref, computed, defineProps, onMounted } from 'vue' +import { ref, computed, onMounted } from 'vue' import type { ComputedRef } from 'vue'; import type { MissionLog } from '../../../../types.ts'; import { diff --git a/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue b/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue index c919d68b6..a288cd0a8 100644 --- a/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue +++ b/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue @@ -19,13 +19,11 @@ /> <div class='ms-auto btn-list'> - <IconRefresh + <TablerIconButton v-if='editLease.id' - :size='32' - stroke='1' - class='cursor-pointer' @click='fetchLease' - /> + ><IconRefresh :size='32' stroke='1'/></TablerIconButton> + <TablerDelete v-if='editLease.id' displaytype='icon' @@ -158,7 +156,25 @@ Video Streaming Protocols </div> <template v-if='expired(lease.expiration)'> - <TablerAlert :err='new Error("Expired Lease")'/> + <TablerAlert + title='Expired Lease' + :err='new Error("Renew the lease to continue using the video stream")' + :advanced='false' + /> + + <div class='col-12 d-flex justify-content-center pb-3'> + <TablerEnum + v-model='editLease.duration' + :options='["16 Hours", "12 Hours", "6 Hours", "1 Hour"]' + style='width: 300px;' + /> + </div> + <div class='col-12 d-flex justify-content-center'> + <button + class='btn btn-primary' + style='width: 280px' + >Renew Lease</button> + </div> </template> <template v-else> <div @@ -176,7 +192,7 @@ </div> <div class='modal-footer'> <button - v-if='protocols.rtsp' + v-if='protocols.rtsp && !expired(lease.expiration)' class='btn btn-secondary' @click='wizard = 1' > @@ -200,7 +216,7 @@ <script setup lang='ts'> import { std } from '../../../../std.ts'; import CopyField from '../../util/CopyField.vue'; -import { defineProps, defineEmits, ref, onMounted } from 'vue'; +import { ref, onMounted } from 'vue'; import type { VideoLease, VideoLeaseResponse, VideoLeaseProtocols } from '../../../../types.ts'; import { IconRefresh, @@ -211,6 +227,7 @@ import { IconChevronDown, } from '@tabler/icons-vue'; import { + TablerIconButton, TablerLoading, TablerAlert, TablerModal, From 4ce23ae05ba321ee2bd86a01ff235d3557e2ad6f Mon Sep 17 00:00:00 2001 From: ingalls <nick@ingalls.ca> Date: Thu, 17 Oct 2024 11:06:31 -0600 Subject: [PATCH 3/4] Refresh inline on Renewal call --- api/routes/video-lease.ts | 18 ++++++++-- .../CloudTAK/Menu/Videos/VideoLeaseModal.vue | 33 +++++++++++++------ 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/api/routes/video-lease.ts b/api/routes/video-lease.ts index a6fd79bce..fe3b0e289 100644 --- a/api/routes/video-lease.ts +++ b/api/routes/video-lease.ts @@ -148,6 +148,10 @@ export default async function router(schema: Schema, config: Config) { }), body: Type.Object({ name: Type.Optional(Type.String()), + duration: Type.Optional(Type.Integer({ + minimum: 0, + description: 'Duration in Seconds' + })), }), res: Type.Object({ lease: VideoLeaseResponse, @@ -157,14 +161,24 @@ export default async function router(schema: Schema, config: Config) { try { const user = await Auth.as_user(config, req); + if (user.access !== AuthUserAccess.ADMIN && req.body.duration > 60 * 60 * 16) { + throw new Err(400, null, 'Only Administrators can request a lease > 16 hours') + } + let lease; if (user.access === AuthUserAccess.ADMIN) { - lease = await config.models.VideoLease.commit(req.params.lease, req.body); + lease = await config.models.VideoLease.commit(req.params.lease, { + ...req.body, + expiration: moment().add(req.body.duration, 'seconds').toISOString(), + }); } else { lease = await config.models.VideoLease.from(req.params.lease); if (lease.username === user.email) { - lease = await config.models.VideoLease.commit(req.params.lease, req.body); + lease = await config.models.VideoLease.commit(req.params.lease, { + ...req.body, + expiration: moment().add(req.body.duration, 'seconds').toISOString(), + }); } else { throw new Err(400, null, 'You can only update a lease you created'); } diff --git a/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue b/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue index a288cd0a8..f99bd3112 100644 --- a/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue +++ b/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue @@ -171,6 +171,7 @@ </div> <div class='col-12 d-flex justify-content-center'> <button + @click='saveLease(false)' class='btn btn-primary' style='width: 280px' >Renew Lease</button> @@ -204,7 +205,7 @@ </button> <button class='btn btn-primary' - @click='saveLease' + @click='saveLease(true)' > Save </button> @@ -299,18 +300,30 @@ async function deleteLease() { emit('refresh'); } -async function saveLease() { +async function saveLease(close: boolean) { loading.value = true; if (editLease.value.id) { - await std(`/api/video/lease/${editLease.value.id}`, { + const res = await std(`/api/video/lease/${editLease.value.id}`, { method: 'PATCH', - body: editLease.value + body: { + ...editLease.value, + duration: parseInt(editLease.value.duration.split(' ')[0]) * 60 * 60 + } }) as VideoLeaseResponse; - emit('refresh'); + if (close) { + emit('refresh'); + } else { + editLease.value = { + ...res.lease, + duration: '16 Hours' + } + + protocols.value = res.protocols; + } } else { - const { lease } = await std('/api/video/lease', { + const res = await std('/api/video/lease', { method: 'POST', body: { name: editLease.value.name, @@ -318,15 +331,15 @@ async function saveLease() { } }) as VideoLeaseResponse; - if (editLease.value.id) { - emit('refresh') + if (editLease.value.id && close) { + emit('refresh'); } else { editLease.value = { - ...lease, + ...res.lease, duration: '16 Hours' } - await fetchLease(); + protocols.value = res.protocols; } loading.value = false; From 51b6a88fd662488c036776b4324a8415e2e37310 Mon Sep 17 00:00:00 2001 From: ingalls <nick@ingalls.ca> Date: Thu, 17 Oct 2024 11:28:56 -0600 Subject: [PATCH 4/4] Add Lease Renewal --- api/web/package-lock.json | 96 +++++++++---------- .../src/components/CloudTAK/Menu/Videos.vue | 20 +++- .../CloudTAK/Menu/Videos/VideoLeaseModal.vue | 23 +++-- 3 files changed, 80 insertions(+), 59 deletions(-) diff --git a/api/web/package-lock.json b/api/web/package-lock.json index 21e5e2cfa..248dd1f9d 100644 --- a/api/web/package-lock.json +++ b/api/web/package-lock.json @@ -4041,17 +4041,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.9.0.tgz", - "integrity": "sha512-Y1n621OCy4m7/vTXNlCbMVp87zSd7NH0L9cXD8aIpOaNlzeWxIK4+Q19A68gSmTNRZn92UjocVUWDthGxtqHFg==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.10.0.tgz", + "integrity": "sha512-phuB3hoP7FFKbRXxjl+DRlQDuJqhpOnm5MmtROXyWi3uS/Xg2ZXqiQfcG2BJHiN4QKyzdOJi3NEn/qTnjUlkmQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.9.0", - "@typescript-eslint/type-utils": "8.9.0", - "@typescript-eslint/utils": "8.9.0", - "@typescript-eslint/visitor-keys": "8.9.0", + "@typescript-eslint/scope-manager": "8.10.0", + "@typescript-eslint/type-utils": "8.10.0", + "@typescript-eslint/utils": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -4075,16 +4075,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.9.0.tgz", - "integrity": "sha512-U+BLn2rqTTHnc4FL3FJjxaXptTxmf9sNftJK62XLz4+GxG3hLHm/SUNaaXP5Y4uTiuYoL5YLy4JBCJe3+t8awQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.10.0.tgz", + "integrity": "sha512-E24l90SxuJhytWJ0pTQydFT46Nk0Z+bsLKo/L8rtQSL93rQ6byd1V/QbDpHUTdLPOMsBCcYXZweADNCfOCmOAg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.9.0", - "@typescript-eslint/types": "8.9.0", - "@typescript-eslint/typescript-estree": "8.9.0", - "@typescript-eslint/visitor-keys": "8.9.0", + "@typescript-eslint/scope-manager": "8.10.0", + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/typescript-estree": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0", "debug": "^4.3.4" }, "engines": { @@ -4104,14 +4104,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.9.0.tgz", - "integrity": "sha512-bZu9bUud9ym1cabmOYH9S6TnbWRzpklVmwqICeOulTCZ9ue2/pczWzQvt/cGj2r2o1RdKoZbuEMalJJSYw3pHQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.10.0.tgz", + "integrity": "sha512-AgCaEjhfql9MDKjMUxWvH7HjLeBqMCBfIaBbzzIcBbQPZE7CPh1m6FF+L75NUMJFMLYhCywJXIDEMa3//1A0dw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.9.0", - "@typescript-eslint/visitor-keys": "8.9.0" + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4122,14 +4122,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.9.0.tgz", - "integrity": "sha512-JD+/pCqlKqAk5961vxCluK+clkppHY07IbV3vett97KOV+8C6l+CPEPwpUuiMwgbOz/qrN3Ke4zzjqbT+ls+1Q==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.10.0.tgz", + "integrity": "sha512-PCpUOpyQSpxBn230yIcK+LeCQaXuxrgCm2Zk1S+PTIRJsEfU6nJ0TtwyH8pIwPK/vJoA+7TZtzyAJSGBz+s/dg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.9.0", - "@typescript-eslint/utils": "8.9.0", + "@typescript-eslint/typescript-estree": "8.10.0", + "@typescript-eslint/utils": "8.10.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -4147,9 +4147,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.9.0.tgz", - "integrity": "sha512-SjgkvdYyt1FAPhU9c6FiYCXrldwYYlIQLkuc+LfAhCna6ggp96ACncdtlbn8FmnG72tUkXclrDExOpEYf1nfJQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.10.0.tgz", + "integrity": "sha512-k/E48uzsfJCRRbGLapdZgrX52csmWJ2rcowwPvOZ8lwPUv3xW6CcFeJAXgx4uJm+Ge4+a4tFOkdYvSpxhRhg1w==", "dev": true, "license": "MIT", "engines": { @@ -4161,14 +4161,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.9.0.tgz", - "integrity": "sha512-9iJYTgKLDG6+iqegehc5+EqE6sqaee7kb8vWpmHZ86EqwDjmlqNNHeqDVqb9duh+BY6WCNHfIGvuVU3Tf9Db0g==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.10.0.tgz", + "integrity": "sha512-3OE0nlcOHaMvQ8Xu5gAfME3/tWVDpb/HxtpUZ1WeOAksZ/h/gwrBzCklaGzwZT97/lBbbxJ16dMA98JMEngW4w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.9.0", - "@typescript-eslint/visitor-keys": "8.9.0", + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -4229,16 +4229,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.9.0.tgz", - "integrity": "sha512-PKgMmaSo/Yg/F7kIZvrgrWa1+Vwn036CdNUvYFEkYbPwOH4i8xvkaRlu148W3vtheWK9ckKRIz7PBP5oUlkrvQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.10.0.tgz", + "integrity": "sha512-Oq4uZ7JFr9d1ZunE/QKy5egcDRXT/FrS2z/nlxzPua2VHFtmMvFNDvpq1m/hq0ra+T52aUezfcjGRIB7vNJF9w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.9.0", - "@typescript-eslint/types": "8.9.0", - "@typescript-eslint/typescript-estree": "8.9.0" + "@typescript-eslint/scope-manager": "8.10.0", + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/typescript-estree": "8.10.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4252,13 +4252,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.9.0.tgz", - "integrity": "sha512-Ht4y38ubk4L5/U8xKUBfKNYGmvKvA1CANoxiTRMM+tOLk3lbF3DvzZCxJCRSE+2GdCMSh6zq9VZJc3asc1XuAA==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.10.0.tgz", + "integrity": "sha512-k8nekgqwr7FadWk548Lfph6V3r9OVqjzAIVskE7orMZR23cGJjAOVazsZSJW+ElyjfTM4wx/1g88Mi70DDtG9A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.9.0", + "@typescript-eslint/types": "8.10.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -10198,15 +10198,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.9.0.tgz", - "integrity": "sha512-AuD/FXGYRQyqyOBCpNLldMlsCGvmDNxptQ3Dp58/NXeB+FqyvTfXmMyba3PYa0Vi9ybnj7G8S/yd/4Cw8y47eA==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.10.0.tgz", + "integrity": "sha512-YIu230PeN7z9zpu/EtqCIuRVHPs4iSlqW6TEvjbyDAE3MZsSl2RXBo+5ag+lbABCG8sFM1WVKEXhlQ8Ml8A3Fw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.9.0", - "@typescript-eslint/parser": "8.9.0", - "@typescript-eslint/utils": "8.9.0" + "@typescript-eslint/eslint-plugin": "8.10.0", + "@typescript-eslint/parser": "8.10.0", + "@typescript-eslint/utils": "8.10.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/api/web/src/components/CloudTAK/Menu/Videos.vue b/api/web/src/components/CloudTAK/Menu/Videos.vue index 8889fb66b..24f0fcc69 100644 --- a/api/web/src/components/CloudTAK/Menu/Videos.vue +++ b/api/web/src/components/CloudTAK/Menu/Videos.vue @@ -6,13 +6,23 @@ <template #buttons> <TablerIconButton v-if='mode === "lease"' - @click='lease={}' title='Get Lease' - ><IconPlus :size='32' stroke='1'/></TablerIconButton> + @click='lease={}' + > + <IconPlus + :size='32' + stroke='1' + /> + </TablerIconButton> <TablerIconButton - @click='refresh' title='Refresh' - ><IconRefresh :size='32' stroke='1'/></TablerIconButton> + @click='refresh' + > + <IconRefresh + :size='32' + stroke='1' + /> + </TablerIconButton> </template> <template #default> <div @@ -91,8 +101,8 @@ :create='false' /> <div - v-else v-for='l in leases.items' + v-else :key='l.id' class='col-12 py-2 px-3 d-flex align-items-center hover-dark cursor-pointer' @click='lease = l' diff --git a/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue b/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue index f99bd3112..a7f52b78a 100644 --- a/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue +++ b/api/web/src/components/CloudTAK/Menu/Videos/VideoLeaseModal.vue @@ -22,7 +22,12 @@ <TablerIconButton v-if='editLease.id' @click='fetchLease' - ><IconRefresh :size='32' stroke='1'/></TablerIconButton> + > + <IconRefresh + :size='32' + stroke='1' + /> + </TablerIconButton> <TablerDelete v-if='editLease.id' @@ -155,7 +160,7 @@ <div class='subheader pt-4'> Video Streaming Protocols </div> - <template v-if='expired(lease.expiration)'> + <template v-if='expired(editLease.expiration)'> <TablerAlert title='Expired Lease' :err='new Error("Renew the lease to continue using the video stream")' @@ -171,10 +176,12 @@ </div> <div class='col-12 d-flex justify-content-center'> <button - @click='saveLease(false)' class='btn btn-primary' style='width: 280px' - >Renew Lease</button> + @click='saveLease(false)' + > + Renew Lease + </button> </div> </template> <template v-else> @@ -193,7 +200,7 @@ </div> <div class='modal-footer'> <button - v-if='protocols.rtsp && !expired(lease.expiration)' + v-if='protocols.rtsp && !expired(editLease.expiration)' class='btn btn-secondary' @click='wizard = 1' > @@ -251,6 +258,7 @@ const editLease = ref<{ id?: number name: string duration: string + expiration?: string stream_user: string | null stream_pass: string | null }>({ @@ -272,7 +280,8 @@ onMounted(async () => { loading.value = false }); -function expired(expiration: string) { +function expired(expiration?: string) { + if (!expiration) return false; return +new Date(expiration) < +new Date(); } @@ -322,6 +331,8 @@ async function saveLease(close: boolean) { protocols.value = res.protocols; } + + loading.value = true; } else { const res = await std('/api/video/lease', { method: 'POST',