Commit 0e376f4b authored by Andrea Ida Malkah Klaura's avatar Andrea Ida Malkah Klaura
Browse files

Merge branch 'develop'

parents c75a659a 81033f44
......@@ -33,7 +33,17 @@ npm run e2e
npm test
```
## Configuration of the steering/pv backend
## Configuration
All global configuration settings of the dashboard application can be set in the corresponding `config/*.env.js` files. You can use different settings of a `dev`elopment, a `prod`uction and a `test` environment. So for a productive environment you will have to set all values in `config/prod.env.js`. In then `config/dev.env.js` you can overwrite only those value that differ from those in the production setting.
All values are provided with comments in the `config/prod.env.js`, so you can just take a look there. Here are some important notes on what to set and what to not forget.
Most likely the only values that you will have to set in the `dev.env.js` file different from the `prod.env.js` file are those containing links. Also be aware that these settings become environment variables once compiled by _Vue.js_. Therefore an integer is represented as `'23'` while a string is represented as `'"23"'` in the config file. Sometimes this is important.
For the _OpenID Connect_ settings it is very important to use exactly the same redirect URIs as defined in you OIDC client settings in the _aura/steering_ module. So `API_STEERING_OIDC_REDIRECT_URI` and `API_STEERING_OIDC_REDIRECT_URI_SILENT` should ideally be a copy-paste from there. This can be a nasty debug issue if you don't get the login to work. For example we once had the issue that while the _steering_ used `http://localhost:8080/static/oidc_callback.html` as the parameter for the REDIRECT_URI, the dashboard had configured `http://127.0.0.1:8080/static/oidc_callback.html`. You would expect that this resolves to the same location, but even if `localhost` resolves to `127.0.0.1`, the _OIDC provider_ in the _steering_ module does a string comparison of what it receives from the client and what it has configured.
## Configuration of the steering backend
For the dashboard to run in a dev mode you only need the `npm install` and `npm run dev` commands. To access show data in the show manager you also have to have the [steering/pv module](https://gitlab.servus.at/autoradio/pv) running somewhere. There you need to add the following lines to the `pv/local_settings.py`, in order to allow CORS requests from you dashboard:
......
......@@ -4,12 +4,11 @@ const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
OIDC_CLIENT_ID: '"174626"',
API_STEERING: '"http://127.0.0.1:8000/api/v1/"',
API_STEERING_SHOWS: '"http://127.0.0.1:8000/api/v1/shows/"',
// OIDC endpoint of the pv/steering module
API_STEERING_OIDC_URI: '"http://localhost:8000/openid"',
// local callback handler that is called by the pv/steering OIDC module after login
API_STEERING_OIDC_REDIRECT_URI: '"http://localhost:8080/static/oidc_callback.html"',
// address that is called by the pv/steering OIDC module after logout - should be the dashboard entry point
API_STEERING_OIDC_REDIRECT_URI_POSTLOGOUT: '"http://localhost:8080"'
API_STEERING_OIDC_REDIRECT_URI_SILENT: '"http://localhost:8080/static/oidc_callback_silentRenew.html"',
API_STEERING_OIDC_REDIRECT_URI_POSTLOGOUT: '"http://localhost:8080"',
})
'use strict'
module.exports = {
NODE_ENV: '"production"'
NODE_ENV: '"production"',
/*
aura/steering REST API settings
===============================
*/
// These are the REST API endpoints of your aura/steering module
API_STEERING: '"http://YOUR.STEERING.DOMAIN/api/v1/"',
API_STEERING_SHOWS: '"http://YOUR.STEERING.DOMAIN/api/v1/shows/"',
/*
Open ID Connect settings
========================
*/
// Put your OpenID Connect client ID here. You get it in the setup of the aura/steering module.
OIDC_CLIENT_ID: '"174626"',
// OIDC endpoint of the pv/steering module
API_STEERING_OIDC_URI: '"http://YOUR.STEERING.DOMAIN/openid"',
// Number of seconds before token gets invalid, when renewal should be started
API_STEERING_OIDC_EXPIRE_NOTIFICATION: '120',
// Local callback handlers that are called by the aura/steering OIDC module after login/renwal.
// It is important to put exactly the same URI here as it is configured in your OIDC client settings
// in the aura/steering module. Don't mix IPs and DNS names!
API_STEERING_OIDC_REDIRECT_URI: '"http://FINAL.DASHBOARD.DOMAIN/static/oidc_callback.html"',
API_STEERING_OIDC_REDIRECT_URI_SILENT: '"http://FINAL.DASHBOARD.DOMAIN/static/oidc_callback_silentRenew.html"',
// address that is called by the pv/steering OIDC module after logout - should be the dashboard entry point
API_STEERING_OIDC_REDIRECT_URI_POSTLOGOUT: '"http://FINAL.DASHBOARD.DOMAIN"',
/*
Dashboard UI defaults
=====================
*/
// How many timeslots should be shown by default? (has to be a string)
TIMESLOT_FILTER_DEFAULT_NUMSLOTS: '"10"',
// For how many days from now should timeslots be fetched by default? (has to be an int)
TIMESLOT_FILTER_DEFAULT_DAYS: '60'
}
......@@ -12,6 +12,7 @@ import 'bootstrap-vue/dist/bootstrap-vue.css'
import oidc from 'oidc-client'
import header from './components/Header.vue'
import footer from './components/Footer.vue'
import axios from 'axios'
export default {
name: 'app',
......@@ -38,15 +39,20 @@ export default {
name: '',
email: '',
access_token: '',
logged_in: false
expires_at: 0,
logged_in: false,
steeringUser: null
},
userOIDC: null,
mgr: new oidc.UserManager({
oidcmgr: new oidc.UserManager({
userStore: new oidc.WebStorageStateStore(),
authority: process.env.API_STEERING_OIDC_URI,
client_id: '174626',
// the client id has to be a string, therefore we add the + ''
client_id: process.env.OIDC_CLIENT_ID,
redirect_uri: process.env.API_STEERING_OIDC_REDIRECT_URI,
// redirect_uri: process.env.API_STEERING_OIDC_REDIRECT_URI,
silent_redirect_uri: 'http://localhost:8080/static/oidc_callback_silentRenew.html',
popup_redirect_uri: 'http://localhost:8080/static/oidc_callback_popupRenew.html',
accessTokenExpiringNotificationTime: process.env.API_STEERING_OIDC_EXPIRE_NOTIFICATION,
response_type: 'id_token token',
scope: 'openid profile email',
post_logout_redirect_uri: process.env.API_STEERING_OIDC_REDIRECT_URI_POSTLOGOUT,
......@@ -62,41 +68,92 @@ export default {
},
methods: {
signIn () {
this.mgr.signinRedirect().catch(function (err) {
this.oidcmgr.signinRedirect().catch(function (err) {
console.log(err)
})
},
signOut () {
let self = this
this.mgr.signoutRedirect().then(function (resp) {
this.oidcmgr.signoutRedirect().then(function (resp) {
self.user.logged_in = false
console.log('signed out', resp)
}).catch(function (err) {
console.log(err)
})
},
getUser () {
getSteeringUser () {
axios.get(process.env.API_STEERING + 'users/', {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.user.access_token }
}).then(response => {
if (response.data.length === 0) {
alert('No user profile data provided by steering backend!')
} else if (response.data.length === 1) {
this.user.steeringUser = response.data[0]
} else {
// in case we are a superuser, we get all users returned
// so we have to iterate through the user list to find out own profile
for (var u in response.data) {
if (response.data[u].username === this.user.name) {
this.user.steeringUser = response.data[u]
break
}
}
}
}).catch(error => {
alert('There was an error fetching user data from the steering backend: ' + error)
})
},
getOIDCUser () {
let self = this
this.mgr.getUser().then(function (u) {
if (u == null) {
this.oidcmgr.getUser().then(function (user) {
if (user == null) {
self.user.logged_in = false
self.user.name = ''
self.user.email = ''
self.user.access_token = ''
} else {
self.userOIDC = u
self.user.logged_in = true
self.user.name = u.profile.nickname
self.user.email = u.profile.email
self.user.access_token = u.access_token
// TODO: check user.expires_at
// if token already expired try to get a new one or mark the user as logged out
self.setUserProperties(user)
self.getSteeringUser()
}
}).catch(function (err) {
console.log(err)
})
},
setUserProperties (user) {
this.userOIDC = user
this.user.logged_in = true
this.user.name = user.profile.nickname
this.user.email = user.profile.email
this.user.access_token = user.access_token
// TODO: remove debug info after thorough testing
console.log(new Date(user.expires_at * 1000).toString())
console.log(new Date(user.expires_at * 1000).toUTCString())
console.log(user.access_token)
}
},
mounted () {
this.getUser()
// TODO: remove oidc logging after thorough testing
oidc.Log.logger = console
let self = this
this.oidcmgr.events.addAccessTokenExpiring(function () {
console.log('starting silent access_token renewal')
self.oidcmgr.signinSilent().then(function (user) {
self.user.access_token = user.access_token
console.log(self.user.access_token)
}).catch(function (err) {
console.log(err)
alert('Your OpenID access token could not be renewed automatically.\n' +
'You will be logged out in ' + process.env.API_STEERING_OIDC_EXPIRE_NOTIFICATION + ' seconds.')
})
})
this.oidcmgr.events.addAccessTokenExpired(function () {
console.log('expired!')
self.signOut()
})
this.getOIDCUser()
}
}
</script>
......
......@@ -36,7 +36,7 @@
<b-dropdown-item href="#">Profile</b-dropdown-item>
<b-dropdown-item @click='$parent.signOut'>Signout</b-dropdown-item>
</b-nav-item-dropdown>
<b-nav-item v-if="! user.logged_in" to="login"><img src="../assets/16x16/system-users.png" alt="log-in symbol" title="Log in"></b-nav-item>
<b-nav-item v-if="! user.logged_in" to="home"><img src="../assets/16x16/system-users.png" alt="log-in symbol" title="Log in"></b-nav-item>
<div class="help-image-container">
<b-nav-item>
<router-link to="help"><img class="help-image" src="../assets/help-browser-32x32.png" alt="Help symbol" title="Go to help pages"></router-link>
......
......@@ -47,11 +47,35 @@
<app-modalNotes ref="appModalNotes" :show="shows[currentShow]" :showAggregate="current"></app-modalNotes>
<!-- here we show our table of timeslots -->
<p>
The next <b>{{ numSlots }}</b> timeslots between <b>{{ prettyDate(dateSlotsStart) }}</b> and <b>{{ prettyDate(dateSlotsEnd) }}</b>:
</p>
<b-card>
<b-btn v-b-toggle.timeslotFilterCollapse>Toggle timeslot filters</b-btn>
<b-collapse id="timeslotFilterCollapse">
<br />
<b-row>
<b-col sm="3"><label for="inputNumSlots">Number of slots to show:</label></b-col>
<b-col sm="9"><b-form-input id="inputNumSlots" v-model="numSlots" type="number"></b-form-input></b-col>
</b-row>
<b-row>
<b-col sm="3"><label for="inputDateStart">From:</label></b-col>
<b-col sm="9"><b-form-input id="inputDateStart" v-model="dateStart" type="date"></b-form-input></b-col>
</b-row>
<b-row>
<b-col sm="3"><label for="inputNumSlots">Until (exclusive):</label></b-col>
<b-col sm="9"><b-form-input id="inputDateEnd" v-model="dateEnd" type="date"></b-form-input></b-col>
</b-row>
<br />
<b-container fluid class="text-right">
<b-btn variant="outline-danger" @click="resetFilter()">Reset filter</b-btn> &nbsp;
<b-btn variant="info" @click="applyFilter()">Apply filter</b-btn>
</b-container>
</b-collapse>
</b-card>
<br />
<div v-if="loaded.timeslots">
<b-table striped hover outlined :items="notesTableArray"></b-table>
<b-pagination align="center" :total-rows="current.timeslotmeta.count" :per-page="current.timeslotmeta.perpage" v-model="current.timeslotmeta.page" @change="timeslotsPage"></b-pagination>
</div>
<div v-else style="text-align: center;"><img src="../assets/radio.gif" alt="loading data" /><br /></div>
......@@ -85,7 +109,7 @@
<span v-if="loaded.type">
<span v-if="current.type.length === 0"><small><i>(none set)</i></small></span>
<span v-else>{{ current.type[0].type }}</span>
<img src="../assets/16x16/emblem-system.png" alt="edit" v-on:click="notYetImplemented" />
<img src="../assets/16x16/emblem-system.png" alt="edit" v-on:click="$refs.appModalShow.showType()" />
</span>
<span v-else><img src="../assets/radio.gif" height="24px" alt="loading data" /></span>
</p>
......@@ -98,7 +122,7 @@
<span v-if="loaded.fundingcategory">
<span v-if="current.fundingcategory.length === 0"><small><i>(none set)</i></small></span>
<span v-else>{{ current.fundingcategory[0].fundingcategory }}</span>
<img src="../assets/16x16/emblem-system.png" alt="edit" v-on:click="notYetImplemented" />
<img src="../assets/16x16/emblem-system.png" alt="edit" v-on:click="$refs.appModalShow.showFundingCategory()" />
</span>
<span v-else><img src="../assets/radio.gif" height="24px" alt="loading data" /></span>
</p>
......@@ -110,8 +134,8 @@
<!-- TODO: fetch name for predecessor_id from steering api -->
<b-badge variant="light">Predecessor:</b-badge>
<span v-if="shows[currentShow].predecessor_id === null"><small><i>This show has no predecessor show.</i></small></span>
<span v-else>{{ shows[currentShow].predecessor_id }}</span>
<img src="../assets/16x16/emblem-system.png" alt="edit" v-on:click="notYetImplemented" />
<span v-else>{{ predecessorName }}</span>
<img src="../assets/16x16/emblem-system.png" alt="edit" v-on:click="$refs.appModalShow.showPredecessor()" />
</p>
</b-col>
......@@ -138,7 +162,7 @@
<b-row>
<b-col lg="2">
<b-badge style="width:80%;">Categories:</b-badge> <img src="../assets/16x16/emblem-system.png" alt="edit" v-on:click="notYetImplemented" />
<b-badge style="width:80%;">Categories:</b-badge> <img src="../assets/16x16/emblem-system.png" alt="edit" v-on:click="$refs.appModalShow.showCategories()" />
</b-col>
<b-col lg="4">
<div v-if="loaded.categories">
......@@ -155,7 +179,7 @@
</b-col>
<b-col lg="2">
<b-badge style="width:80%;">Topics:</b-badge> <img src="../assets/16x16/emblem-system.png" alt="edit" v-on:click="notYetImplemented" />
<b-badge style="width:80%;">Topics:</b-badge> <img src="../assets/16x16/emblem-system.png" alt="edit" v-on:click="$refs.appModalShow.showTopics()" />
</b-col>
<b-col lg="4">
<div v-if="loaded.topics">
......@@ -172,7 +196,7 @@
</b-col>
<b-col lg="2">
<b-badge style="width:80%;">Musicfocus:</b-badge> <img src="../assets/16x16/emblem-system.png" alt="edit" v-on:click="notYetImplemented" />
<b-badge style="width:80%;">Musicfocus:</b-badge> <img src="../assets/16x16/emblem-system.png" alt="edit" v-on:click="$refs.appModalShow.showMusicFocus()" />
</b-col>
<b-col lg="4">
<div v-if="loaded.musicfocus">
......@@ -189,7 +213,7 @@
</b-col>
<b-col lg="2">
<b-badge style="width:80%;">Languages:</b-badge> <img src="../assets/16x16/emblem-system.png" alt="edit" v-on:click="notYetImplemented" />
<b-badge style="width:80%;">Languages:</b-badge> <img src="../assets/16x16/emblem-system.png" alt="edit" v-on:click="$refs.appModalShow.showLanguages()" />
</b-col>
<b-col lg="4">
<div v-if="loaded.languages">
......@@ -206,7 +230,7 @@
</b-col>
<b-col lg="2">
<b-badge style="width:80%;">Hosts:</b-badge> <img src="../assets/16x16/emblem-system.png" alt="edit" v-on:click="notYetImplemented" />
<b-badge style="width:80%;">Hosts:</b-badge> <img src="../assets/16x16/emblem-system.png" alt="edit" v-on:click="$refs.appModalShow.showHosts()" />
</b-col>
<b-col lg="4">
<div v-if="loaded.hosts">
......@@ -245,11 +269,9 @@ export default {
shows: [], // an array of objects describing our shows (empty at load, will be populated on created())
currentShow: 0, // index of the currently selected show in our shows array
currentShowID: 0, // actual id of the currently selected show
numUpcoming: 8,
numRecent: 8,
numSlots: 10,
dateSlotsStart: new Date(),
dateSlotsEnd: new Date(new Date().getTime() + 90 * 86400000),
numSlots: process.env.TIMESLOT_FILTER_DEFAULT_NUMSLOTS, // all form input values are provided as strings
dateStart: this.apiDate(new Date()),
dateEnd: this.apiDate(new Date(new Date().getTime() + process.env.TIMESLOT_FILTER_DEFAULT_DAYS * 86400000)),
loaded: {
shows: false,
timeslots: false,
......@@ -274,7 +296,9 @@ export default {
timeslotmeta: { // meta info when pagination is used
count: 0,
next: null,
previous: null
previous: null,
page: 1, // page indexes start at 1 for <b-pagination> components
perpage: 10
},
note: {},
notes: []
......@@ -283,6 +307,14 @@ export default {
},
mixins: [ timeslotSort, prettyDate ],
computed: {
predecessorName: function () {
for (var i in this.shows) {
if (this.shows[i].id === this.shows[this.currentShow].predecessor_id) {
return this.shows[i].name
}
}
return 'Name of predecessor show not available'
},
notesTableArray: function () {
var arr = []
for (var i in this.current.timeslots) {
......@@ -291,20 +323,37 @@ export default {
arr.push({
title: note,
starts: this.prettyDateTime(this.current.timeslots[i].start),
duration: this.prettyDuration(this.current.timeslots[i].start, this.current.timeslots[i].end) + 'min',
duration: this.prettyDuration(this.current.timeslots[i].start, this.current.timeslots[i].end),
// TODO: find out how to insert images or iconffont icons into b-table rows
// options: '<img src="../assets/16x16/emblem-system.png" alt="edit note" v-on:click="' + this.editTimeslotNote(this.current.timeslots[i].id) + '" />'
options: '<span class="timeslotEditLink" onclick="' +
'document.getElementById(\'app\').children[1].__vue__.' +
'editTimeslotNote(' + this.current.timeslots[i].id + ', ' + this.current.timeslots[i].schedule + ')">edit</span> ' +
'<span class="timeslotEditLink" onclick="alert(\'notYetImplemented\')">upload</span>' +
'<span class="timeslotEditLink" onclick="alert(\'notYetImplemented\')">...</span>'
'document.getElementById(\'app\').children[1].__vue__.' +
'editTimeslotNote(' + this.current.timeslots[i].id + ', ' + this.current.timeslots[i].schedule + ')">edit</span> ' +
'<span class="timeslotEditLink" onclick="alert(\'notYetImplemented\')">upload</span>' +
'<span class="timeslotEditLink" onclick="alert(\'notYetImplemented\')">...</span>'
})
}
return arr
}
},
methods: {
applyFilter: function () {
this.current.timeslotmeta.page = 1
this.getTimeslots(this.dateStart, this.dateEnd, this.numSlots)
},
resetFilter: function () {
this.numSlots = process.env.TIMESLOT_FILTER_DEFAULT_NUMSLOTS
this.dateStart = this.apiDate(new Date())
this.dateEnd = this.apiDate(new Date(new Date().getTime() + process.env.TIMESLOT_FILTER_DEFAULT_DAYS * 86400000))
this.current.timeslotmeta.page = 1
this.getTimeslots(this.dateStart, this.dateEnd, this.numSlots)
},
timeslotsPage: function (page) {
if (this.current.timeslotmeta.page !== page) {
this.current.timeslotmeta.page = page
this.getTimeslots(this.dateStart, this.dateEnd, this.numSlots, (page - 1) * this.numSlots)
}
},
switchShow: function (index) {
// if we already had some show loaded with timeslots and notes, set these to
// not loaded, so we don't display old timeslots and notes while already
......@@ -327,33 +376,38 @@ export default {
this.getLanguages()
this.getTopics()
this.getMusicfocus()
this.getRTRCategory()
this.getFundingCategory()
this.getType()
// now fetch the timeslots (including notes) for a given show from PV backend
this.getTimeslots(this.dateSlotsStart, this.dateSlotsEnd, this.numSlots)
this.getTimeslots(this.dateStart, this.dateEnd, this.numSlots)
},
getTimeslots: function (start, end, limit, offset) {
var dateRegex = new RegExp('^\\d{4}-\\d{2}-\\d{2}$')
var uri = process.env.API_STEERING_SHOWS + this.currentShowID + '/timeslots/?'
if (typeof start === 'object') uri += 'start=' + this.apiDate(start)
if (typeof end === 'object') uri += '&end=' + this.apiDate(end)
if (typeof limit === 'number') uri += '&limit=' + limit
if (typeof offset === 'number') uri += '&offset=' + offset
if (dateRegex.test(start)) uri += 'start=' + start + '&'
if (dateRegex.test(end)) uri += 'end=' + end + '&'
if (!isNaN(parseInt(limit))) uri += 'limit=' + parseInt(limit) + '&'
if (!isNaN(parseInt(offset))) uri += 'offset=' + parseInt(offset)
this.loaded.timeslots = false
this.loaded.notes = false
axios.get(uri, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
}).then(response => {
// if we use the limit argument results are paginated and look different
// than without the limit argument
if (typeof limit === 'number') {
if (!isNaN(parseInt(limit))) {
this.current.timeslots = response.data.results
this.current.timeslotmeta.count = response.data.count
this.current.timeslotmeta.next = response.data.next
this.current.timeslotmeta.previous = response.data.previous
this.current.timeslotmeta.perpage = parseInt(limit)
} else {
this.current.timeslots = response.data
this.current.timeslotmeta.count = response.data.length
this.current.timeslotmeta.next = null
this.current.timeslotmeta.previous = null
this.current.timeslotmeta.perpage = response.data.length
}
this.loaded.timeslots = true
// now that we have the timeslots we can fetch notes for all those timeslots
......@@ -386,7 +440,8 @@ export default {
this.current.notes = []
}
}).catch(error => {
alert('There was an error fetching timeslots from the server' + error)
console.log(error)
alert('There was an error fetching timeslots from the server\n' + error)
})
// done fetching timeslots
},
......@@ -524,7 +579,7 @@ export default {
}
if (!loadingError) this.loaded.musicfocus = true
},
getRTRCategory: function () {
getFundingCategory: function () {
this.current.fundingcategory = []
var loadingError = false
if (typeof this.shows[this.currentShow].fundingcategory !== 'number') {
......@@ -537,7 +592,7 @@ export default {
this.current.fundingcategory.push(response.data)
}).catch(error => {
loadingError = true
alert('There was an error fetching RTR category from the server: ' + error)
alert('There was an error fetching funding category from the server: ' + error)
})
}
if (!loadingError) this.loaded.fundingcategory = true
......@@ -565,10 +620,18 @@ export default {
}
},
created () {
axios.get(process.env.API_STEERING_SHOWS, {
var uri = process.env.API_STEERING_SHOWS
if (!this.$parent.user.steeringUser.is_superuser) {
uri += '?owner=' + this.$parent.user.steeringUser.id
}
axios.get(uri, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
}).then(response => {
if (response.data.length === 0) {
alert('There are now shows associated with your account!')
return
}
this.shows = response.data
this.currentShowID = this.shows[0].id
this.currentShow = 0
......
This diff is collapsed.
......@@ -55,19 +55,19 @@ export default {
dstring += leadingZero(d.getDate()) + '. '
dstring += month[d.getMonth()] + ' '
dstring += d.getFullYear() + ', '
dstring += leadingZero(d.getHours()) + ':' + leadingZero(d.getMinutes())
dstring += leadingZero(d.getHours()) + ':' + leadingZero(d.getMinutes()) + ':' + leadingZero(d.getSeconds())
return dstring
},
prettyDuration: function (start, end) {
var duration = (new Date(end).getTime() - new Date(start).getTime()) / 1000
/*
// This is the old notation in HH:MM - have to decide which one to use in final design
var hours = Math.floor(duration / 60 / 60)
var minutes = (duration / 60) % 60
return leadingZero(hours) + ':' + leadingZero(minutes)
*/
// Here we just return the duration in minutes
return duration / 60
var seconds = (new Date(end).getTime() - new Date(start).getTime()) / 1000
var minutes = Math.floor(seconds / 60)
var duration = minutes + 'min'
if (minutes < seconds / 60) {
duration += ' '
duration += seconds - 60 * minutes
duration += 'sec'
}
return duration
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Waiting...</title>
</head>
<body>
<script src="oidc-client.js"></script>
<script>
var mgr = new Oidc.UserManager()
mgr.signinPopupCallback()
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Waiting...</title>
</head>
<body>
<script src="oidc-client.js"></script>
<script>
var mgr = new Oidc.UserManager()
mgr.signinSilentCallback()
</script>
</body>
</html>
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment