- TT#115519 Create separate page and route "Recordings" under "Call Settings" - TT#115520 Integrate and configure QTable - TT#115521 Implement vuex actions, mutations and getters - TT#115522 Implement expanded section to render Streams meta data NOTE #1: for populating recording table, please follow these instructions https://sipwise.atlassian.net/wiki/spaces/DEV/pages/1105494098/Recorded+calls+on+dev+environment NOTE #2: /callrecordingstreams endpoint needs to be fixed (TT#118600 ); in the meantime as workaround to be able to fetch streams you need to go to /usr/share/perl5/NGCP/Panel/Controller/API/CallRecordingStreams.pm on your VM and modify line 30, change 'call' to 'me.call', then execute systemctl restart ngcp-panel Change-Id: Iac102ef7449cf7621166f2492cd8b200005768e1mr9.4
parent
571ba6917f
commit
69ab2d448a
@ -0,0 +1,263 @@
|
|||||||
|
<template>
|
||||||
|
<csc-page
|
||||||
|
class="q-pa-lg"
|
||||||
|
>
|
||||||
|
<template>
|
||||||
|
<div class="q-pa-md">
|
||||||
|
<q-table
|
||||||
|
:data="data"
|
||||||
|
:columns="columns"
|
||||||
|
:loading="$wait.is('csc-call-recordings')"
|
||||||
|
row-key="name"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th auto-width />
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
class="table-th"
|
||||||
|
>
|
||||||
|
{{ col.label }}
|
||||||
|
</q-th>
|
||||||
|
<q-th auto-width />
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
v-slot:body="props"
|
||||||
|
>
|
||||||
|
<q-tr
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
:icon="isRowExpanded(props.row.id) ? 'remove' : 'add'"
|
||||||
|
@click="updateCollapseArray(props.row.id)"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
<q-td
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
{{ col.value }}
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<csc-more-menu>
|
||||||
|
<csc-popup-menu-item-delete
|
||||||
|
color="negative"
|
||||||
|
@click="confirmRowDeletion(props.row.id)"
|
||||||
|
/>
|
||||||
|
</csc-more-menu>
|
||||||
|
<csc-confirmation-dialog
|
||||||
|
:key="props.row.id"
|
||||||
|
:ref="'confirmDelete-'+props.row.id"
|
||||||
|
title-icon="delete"
|
||||||
|
title-icon-color="negative"
|
||||||
|
color="negative"
|
||||||
|
:title="$t('Delete recording', {id: props.row.id})"
|
||||||
|
:message="$t('You are about to delete recording #{id}', {id: props.row.id})"
|
||||||
|
@confirm="deleteRecord(props.row.id)"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
<q-tr
|
||||||
|
v-show="isRowExpanded(props.row.id)"
|
||||||
|
no-hover
|
||||||
|
>
|
||||||
|
<q-td
|
||||||
|
colspan="100%"
|
||||||
|
class="table-td-no-padding"
|
||||||
|
>
|
||||||
|
<q-table
|
||||||
|
:data="props.row.files"
|
||||||
|
:columns="filesColumns"
|
||||||
|
:loading="$wait.is('csc-call-recordings')"
|
||||||
|
:hide-pagination="true"
|
||||||
|
row-key="name"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
{{ col.label }}
|
||||||
|
</q-th>
|
||||||
|
<q-th auto-width />
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
{{ col.value }}
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<q-btn
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
icon="download"
|
||||||
|
dense
|
||||||
|
@click="saveFile(props.row.id)"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</csc-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
mapGetters
|
||||||
|
} from 'vuex'
|
||||||
|
import {
|
||||||
|
mapWaitingActions
|
||||||
|
} from 'vue-wait'
|
||||||
|
import CscPage from 'components/CscPage'
|
||||||
|
import CscMoreMenu from 'components/CscMoreMenu'
|
||||||
|
import CscPopupMenuItemDelete from 'components/CscPopupMenuItemDelete'
|
||||||
|
import CscConfirmationDialog from 'components/CscConfirmationDialog'
|
||||||
|
import { showGlobalError, showToast } from 'src/helpers/ui'
|
||||||
|
import { saveAs } from 'file-saver'
|
||||||
|
export default {
|
||||||
|
name: 'CscCallBlocking',
|
||||||
|
components: {
|
||||||
|
CscPage,
|
||||||
|
CscMoreMenu,
|
||||||
|
CscPopupMenuItemDelete,
|
||||||
|
CscConfirmationDialog
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
required: true,
|
||||||
|
label: this.$t('Id'),
|
||||||
|
align: 'left',
|
||||||
|
field: row => row.id,
|
||||||
|
format: val => `${val}`,
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'time',
|
||||||
|
required: true,
|
||||||
|
align: 'left',
|
||||||
|
label: this.$t('Time'),
|
||||||
|
field: row => row.time,
|
||||||
|
sortable: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
filesColumns: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
required: true,
|
||||||
|
label: '#',
|
||||||
|
align: 'left',
|
||||||
|
field: row => row.id,
|
||||||
|
format: val => `${val}`,
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
required: true,
|
||||||
|
align: 'left',
|
||||||
|
label: this.$t('Type'),
|
||||||
|
field: row => row.type,
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'format',
|
||||||
|
required: true,
|
||||||
|
align: 'left',
|
||||||
|
label: this.$t('Format'),
|
||||||
|
field: row => row.format,
|
||||||
|
sortable: true
|
||||||
|
}
|
||||||
|
|
||||||
|
],
|
||||||
|
data: [],
|
||||||
|
rowStatus: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('callRecordings', [
|
||||||
|
'recordings'
|
||||||
|
])
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
recordings () {
|
||||||
|
this.data = this.recordings
|
||||||
|
this.rowStatus = this.recordings.map(rec => {
|
||||||
|
return {
|
||||||
|
id: rec.id,
|
||||||
|
expanded: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted () {
|
||||||
|
await this.fetchRecordings()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapWaitingActions('callRecordings', {
|
||||||
|
fetchRecordings: 'csc-call-recordings',
|
||||||
|
fetchStreams: 'csc-call-recordings',
|
||||||
|
deleteRecording: 'csc-call-recordings',
|
||||||
|
downloadRecording: 'csc-call-recordings'
|
||||||
|
}),
|
||||||
|
confirmRowDeletion (rowId) {
|
||||||
|
this.$refs['confirmDelete-' + rowId].open()
|
||||||
|
},
|
||||||
|
async deleteRecord (rowId) {
|
||||||
|
try {
|
||||||
|
await this.deleteRecording(rowId)
|
||||||
|
showToast(this.$t('Recording successfully deleted'))
|
||||||
|
await this.fetchRecordings()
|
||||||
|
} catch (err) {
|
||||||
|
showGlobalError(this.$t('Something went wrong. Please retry later'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isRowExpanded (id) {
|
||||||
|
const rowStatus = this.rowStatus.filter(row => row.id === id)[0] || null
|
||||||
|
return rowStatus && rowStatus.expanded
|
||||||
|
},
|
||||||
|
updateCollapseArray (id) {
|
||||||
|
const recording = this.recordings.filter(rec => rec.id === id)[0]
|
||||||
|
const rowStatus = this.rowStatus.filter(row => row.id === id)[0]
|
||||||
|
rowStatus.expanded = !rowStatus.expanded
|
||||||
|
if (rowStatus.expanded && recording.files.length === 0) {
|
||||||
|
this.fetchStreams(id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async saveFile (fileId) {
|
||||||
|
const file = await this.downloadRecording(fileId)
|
||||||
|
saveAs(file, 'call-recording-' + fileId + '.wav')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" rel="stylesheet/stylus" scoped>
|
||||||
|
.table-th
|
||||||
|
font-size 15px
|
||||||
|
.table-td-no-padding
|
||||||
|
padding 0px !important // needed to override .q-table td
|
||||||
|
</style>
|
@ -0,0 +1,45 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import { getRecordings, getRecordingStreams, downloadRecordingStream } from '../api/subscriber'
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
recordings: []
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
subscriberId (state, getters, rootState, rootGetters) {
|
||||||
|
return parseInt(rootGetters['user/getSubscriberId'])
|
||||||
|
},
|
||||||
|
recordings (state, getters, rootState, rootGetters) {
|
||||||
|
return state.recordings
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
callRecordings (state, res) {
|
||||||
|
state.recordings = res
|
||||||
|
},
|
||||||
|
callRecordingStreams (state, data) {
|
||||||
|
const recording = state.recordings.filter(rec => rec.id === data.recId)[0]
|
||||||
|
recording.files = data.streams
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async fetchRecordings (context) {
|
||||||
|
const recs = await getRecordings(context.getters.subscriberId)
|
||||||
|
context.commit('callRecordings', recs)
|
||||||
|
},
|
||||||
|
async fetchStreams (context, recId) {
|
||||||
|
const streams = await getRecordingStreams(recId)
|
||||||
|
context.commit('callRecordingStreams', {
|
||||||
|
recId: recId,
|
||||||
|
streams: streams
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async deleteRecording (context, recId) {
|
||||||
|
await Vue.http.delete('api/callrecordings/' + recId + '?force_delete=1')
|
||||||
|
},
|
||||||
|
async downloadRecording (context, fileId) {
|
||||||
|
const fileBody = await downloadRecordingStream(fileId)
|
||||||
|
return fileBody
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue