Merge pull request #106 from nemozak1/develop

Fix enter to submit, and dynamic entities not loading
This commit is contained in:
Simon Redfern 2025-05-22 13:23:53 +02:00 committed by GitHub
commit 04ed8ea0d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 271 additions and 173 deletions

1
components.d.ts vendored
View File

@ -7,6 +7,7 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AutoLogout: typeof import('./src/components/AutoLogout.vue')['default']
ChatMessage: typeof import('./src/components/ChatMessage.vue')['default']
ChatWidget: typeof import('./src/components/ChatWidget.vue')['default']
ChatWidgetOld: typeof import('./src/components/ChatWidgetOld.vue')['default']

View File

@ -1,110 +0,0 @@
import * as countdownTimer from '../../src/assets/inactivity-timer.js'
// holds the idle duration in ms (current value = 301 seconds)
var timeoutIntervalInMillis = 5 * 60 * 1000 + 1000;
// holds the timeout variables for easy destruction and reconstruction of the setTimeout hooks
var timeHook = null;
function initializeTimeHook() {
// this method has the purpose of creating our timehooks and scheduling the call to our logout function when the idle time has been reached
if (timeHook == null) {
timeHook = setTimeout( function () { destroyTimeHook(); logout(); }.bind(this), timeoutIntervalInMillis);
}
}
function destroyTimeHook() {
// this method has the sole purpose of destroying any time hooks we might have created
clearTimeout(timeHook);
timeHook = null;
}
function resetTimeHook(event) {
// this method replaces the current time hook with a new time hook
destroyTimeHook();
initializeTimeHook();
countdownTimer.resetCountdownTimer(timeoutIntervalInMillis / 1000);
// show event type, element and coordinates of the click
// console.log(event.type + " at " + event.currentTarget);
// console.log("Coordinates: " + event.clientX + ":" + event.clientY);
console.log("Reset inactivity of a user");
}
function setupListeners() {
// here we setup the event listener for the mouse click operation
document.addEventListener("click", resetTimeHook);
document.addEventListener("mousemove", resetTimeHook);
document.addEventListener("mousedown", resetTimeHook);
document.addEventListener("keypress", resetTimeHook);
document.addEventListener("touchmove", resetTimeHook);
console.log("Listeners for user inactivity activated");
}
function destroyListeners() {
// here we destroy event listeners for the mouse click operation
document.removeEventListener("click", resetTimeHook);
document.removeEventListener("mousemove", resetTimeHook);
document.removeEventListener("mousedown", resetTimeHook);
document.removeEventListener("keypress", resetTimeHook);
document.removeEventListener("touchmove", resetTimeHook);
console.log("Listeners for user inactivity deactivated");
}
function logout() {
destroyListeners();
countdownTimer.destroyCountdownTimer();
console.log("Logging you out due to inactivity..");
const logoffButton = document.getElementById("logout");
logoffButton.click();
}
async function makeObpApiCall() {
//debug
console.log("calling API");
let timeoutInSeconds;
try {
let obpApiHost = document.getElementById("nav");
console.log(obpApiHost);
if(obpApiHost) {
obpApiHost = obpApiHost.href.split("?")[0];
}
const response = await fetch(`${obpApiHost}/obp/v5.1.0/ui/suggested-session-timeout`);
const json = await response.json();
if(json.timeout_in_seconds) {
timeoutInSeconds = json.timeout_in_seconds;
console.log(`Suggested value ${timeoutInSeconds} is used`);
} else {
timeoutInSeconds = 5 * 60 + 1; // Set default value to 301 seconds
console.log(`Default value ${timeoutInSeconds} is used`);
}
} catch (e) {
console.error(e);
timeoutInSeconds = 5 * 60 + 1; // Set default value to 301 seconds, even if the session timeout endpoint is not reachable for whatever reason
console.log(`Default value ${timeoutInSeconds} is used`);
}
return timeoutInSeconds;
}
async function getSuggestedSessionTimeout() {
if(!sessionStorage.getItem("suggested-session-timeout-in-seconds")) {
let timeoutInSeconds = await makeObpApiCall();
sessionStorage.setItem("suggested-session-timeout-in-seconds", timeoutInSeconds);
}
return sessionStorage.getItem("suggested-session-timeout-in-seconds") * 1000 + 1000; // We need timeout in millis
}
// self executing function to trigger the operation on page load
(async function () {
timeoutIntervalInMillis = await getSuggestedSessionTimeout(); // Try to get suggested value
const logoffButton = document.getElementById("countdown-timer-span");
if(logoffButton) {
// to prevent any lingering timeout handlers preventing memory leaks
destroyTimeHook();
// setup a fresh time hook
initializeTimeHook();
// setup initial event listeners
setupListeners();
// Reset countdown timer
countdownTimer.resetCountdownTimer(timeoutIntervalInMillis / 1000);
}
})();

View File

@ -49,10 +49,17 @@ export class UserController {
this.oauthInjectedService.requestTokenKey = undefined
this.oauthInjectedService.requestTokenSecret = undefined
session['clientConfig'] = undefined
if(!this.obpExplorerHome) {
console.error(`VITE_OBP_API_EXPLORER_HOST: ${this.obpExplorerHome}`)
if (request.query.redirect) {
response.redirect(request.query.redirect as string)
} else {
if(!this.obpExplorerHome) {
console.error(`VITE_OBP_API_EXPLORER_HOST: ${this.obpExplorerHome}`)
}
response.redirect(this.obpExplorerHome)
}
response.redirect(this.obpExplorerHome)
return response
}

View File

@ -64,15 +64,31 @@ export default class OauthAccessTokenMiddleware implements ExpressMiddlewareInte
console.log(`OauthAccessTokenMiddleware.ts use says: clientConfig: ${JSON.stringify(clientConfig)}`)
session['clientConfig'] = clientConfig
console.log('OauthAccessTokenMiddleware.ts use says: Seems OK, redirecting..')
let redirectPage: String
const obpExplorerHome = process.env.VITE_OBP_API_EXPLORER_HOST
if(!obpExplorerHome) {
console.error(`VITE_OBP_API_EXPLORER_HOST: ${obpExplorerHome}`)
}
console.log(`OauthAccessTokenMiddleware.ts use says: Will redirect to: ${obpExplorerHome}`)
if (session['redirectPage']) {
try {
redirectPage = session['redirectPage']
} catch (e) {
console.log('OauthAccessTokenMiddleware.ts use says: Error decoding redirect URI')
redirectPage = obpExplorerHome
}
} else {
redirectPage = obpExplorerHome
}
console.log(`OauthAccessTokenMiddleware.ts use says: Will redirect to: ${redirectPage}`)
console.log('OauthAccessTokenMiddleware.ts use says: Here comes the session:')
console.log(session)
response.redirect(`${obpExplorerHome}`)
response.redirect(redirectPage)
}
}
)

View File

@ -39,6 +39,13 @@ export default class OauthRequestTokenMiddleware implements ExpressMiddlewareInt
console.debug('process.env.VITE_OBP_API_PORTAL_HOST:', process.env.VITE_OBP_API_PORTAL_HOST)
const oauthService = this.oauthInjectedService
const consumer = oauthService.getConsumer()
const redirectPage = request.query.redirect
const session = request.session
if (redirectPage) {
session['redirectPage'] = redirectPage
}
consumer.getOAuthRequestToken((error: any, oauthTokenKey: string, oauthTokenSecret: string) => {
if (error) {
const errorStr = JSON.stringify(error)

View File

@ -1,36 +0,0 @@
function addSeconds(date, seconds) {
date.setSeconds(date.getSeconds() + seconds);
return date;
}
export function showCountdownTimer() {
// Get current date and time
var now = new Date().getTime();
let distance = countDownDate - now;
// Output the result in an element with id="countdown-timer-span"
let elementId = ("countdown-timer-span");
document.getElementById(elementId).innerHTML = "in " + Math.floor(distance / 1000) + "s";
// If the count down is over release resources
if (distance < 0) {
destroyCountdownTimer();
}
}
// Set the date we're counting down to
let countDownDate = addSeconds(new Date(), 5);
let showTimerInterval = null;
export function destroyCountdownTimer() {
clearInterval(showTimerInterval);
}
export function resetCountdownTimer(seconds) {
destroyCountdownTimer(); // Destroy previous timer if any
countDownDate = addSeconds(new Date(), seconds); // Set the date we're counting down to
showTimerInterval = setInterval(showCountdownTimer, 1000); // Update the count down every 1 second
}

View File

@ -0,0 +1,157 @@
<script setup lang="ts">
import { ElNotification, NotificationHandle } from 'element-plus';
import { ref, computed, h, onMounted, onBeforeUnmount } from 'vue';
// Props can be defined with defineProps
const props = defineProps({
// Define your props here
});
// Types of events that will reset the timeout
const events = ['click', 'mousemove', 'keydown', 'keypress', 'mousedown', 'scroll', 'load'];
// Set timers
let warningTimeout: NodeJS.Timeout;
let logoutTimeout: NodeJS.Timeout;
let logoutTime: number;
let countdownInterval: NodeJS.Timeout;
// Add these variables at the top of your script
let defaultWarningDelay = 1000 * 270; // 4.5 minutes by default
let defaultLogoutDelay = 1000 * 300; // 5 minutes by default
// Methods
function setTimers(warningDelay = defaultWarningDelay, logoutDelay = defaultLogoutDelay) {
logoutTime = Date.now() + logoutDelay;
warningTimeout = setTimeout(warningMessage, warningDelay); // 4 seconds for development, change later
logoutTimeout = setTimeout(logout, logoutDelay); // 15 seconds for development, change later
}
let warningNotification: NotificationHandle;
async function getOBPSuggestedTimeout() {
const obpApiHost = import.meta.env.VITE_OBP_API_HOST;
let timeoutInSeconds: number;
// Fetch the suggested timeout from the OBP API
const response = await fetch(`${obpApiHost}/obp/v5.1.0/ui/suggested-session-timeout`);
const json = await response.json();
if(json.timeout_in_seconds) {
timeoutInSeconds = json.timeout_in_seconds;
console.log(`Suggested value ${timeoutInSeconds} is used`);
} else {
timeoutInSeconds = 5 * 60 + 1; // Set default value to 301 seconds
console.log(`Default value ${timeoutInSeconds} is used`);
}
return timeoutInSeconds;
}
function resetTimeout() {
// Logic to reset the timeout
clearTimeout(warningTimeout);
clearTimeout(logoutTimeout);
clearInterval(countdownInterval);
if (warningNotification) {
warningNotification.close();
}
setTimers();
}
function warningMessage() {
// Logic to show warning message
console.log('Warning: You will be logged out soon');
let secondsLeft = ref(Math.ceil((logoutTime - Date.now()) / 1000));
// Update the countdown every second
countdownInterval = setInterval(() => {
secondsLeft.value = Math.ceil((logoutTime - Date.now()) / 1000);
// If time's up or almost up, clear the interval
if (secondsLeft.value <= 0) {
clearInterval(countdownInterval);
return;
}
}, 1000);
warningNotification = ElNotification({
title: 'Inactivity Warning',
message: () => h('p', null, [
h('span', null, 'You will be logged out in'),
h('strong', { style: 'color: red' }, ` ${secondsLeft.value} `),
h('span', null, 'seconds.'),
])
,
type: 'warning',
duration: 0,
position: 'top-left',
showClose: false,
})
}
function logout() {
// Logic to log out the user
console.log('Logging out...');
document.getElementById("logoff")?.click(); // If the ID of the logout button changes, this will not work
}
// Lifecycle hooks
onMounted(() => {
events.forEach(event => {
window.addEventListener(event, resetTimeout);
})
setTimers();
// Update with API suggested values when available
getOBPSuggestedTimeout().then(timeoutInSeconds => {
// Convert to milliseconds
const logoutDelay = timeoutInSeconds * 1000;
// Set warning to appear 30 seconds before logout
const warningDelay = Math.max(logoutDelay - 30000, 0);
// Update the defaults
defaultWarningDelay = warningDelay;
defaultLogoutDelay = logoutDelay;
// Reset timers with new values
resetTimeout();
}).catch(error => {
console.error("Failed to get suggested timeout:", error);
// Continue with defaults
});
});
onBeforeUnmount(() => {
// Cleanup code before component is unmounted
clearTimeout(warningTimeout);
clearTimeout(logoutTimeout);
clearInterval(countdownInterval);
events.forEach(event => {
window.removeEventListener(event, resetTimeout);
});
});
</script>
<style scoped>
/* Your component styles here */
</style>
<template>
<div>
<!-- Your component content here -->
</div>
</template>

View File

@ -4,7 +4,7 @@ placeholder for Opey II Chat widget
<script lang="ts">
import { ref, reactive } from 'vue'
import { Close, Top as ElTop } from '@element-plus/icons-vue'
import { Close, Top as ElTop, WarnTriangleFilled } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import ChatMessage from './ChatMessage.vue';
import { v4 as uuidv4 } from 'uuid';
@ -17,6 +17,7 @@ export default {
return {
Close,
ElTop,
WarnTriangleFilled,
}
},
data() {
@ -24,6 +25,7 @@ export default {
chatOpen: false,
input: '',
lastUserMessasgeFailed: false,
errorState: <{ type?: "authenticationError", message?: string, icon?: any }> {}, // add types of error as needed
chat: useChat(),
}
},
@ -34,15 +36,7 @@ export default {
this.chat = useChat()
const isLoggedIn = await this.checkLoginStatus()
console.log('Is logged in: ', isLoggedIn)
if (isLoggedIn) {
try {
await this.chat.handleAuthentication()
} catch (error) {
console.error('Error in chat:', error);
ElMessage.error('Failed to authenticate.')
}
}
},
methods: {
async toggleChat() {
@ -53,7 +47,15 @@ export default {
const currentResponseKeys = Object.keys(currentUser)
if (currentResponseKeys.includes('username')) {
if (!this.chat.userIsAuthenticated) {
await this.chat.handleAuthentication()
try {
await this.chat.handleAuthentication()
} catch (error) {
console.error('Error in chat:', error);
this.errorState.type = "authenticationError"
this.errorState.message = "Woops! Looks like we are having trouble connecting to Opey..."
this.errorState.icon = WarnTriangleFilled
}
}
return true
} else {
@ -114,7 +116,14 @@ export default {
<el-button type="danger" :icon="Close" @click="toggleChat" size="small" circle></el-button>
</el-header>
<el-main>
<div v-if="!chat.userIsAuthenticated" class="login-container">
<div v-if="errorState.type === 'authenticationError'" class="login-container">
<el-icon :size="40" color="#FF4D4F">
<component :is="errorState.icon" />
</el-icon>
<p class="login-message" size="large">{{ errorState.message }}</p>
</div>
<div v-else-if="!chat.userIsAuthenticated" class="login-container">
<p class="login-message" size="large">Opey is only available once logged on.</p>
<a href="/api/connect" class="login-button router-link">Log on</a>
</div>

View File

@ -60,7 +60,8 @@ const headerLinksBackgroundColor = ref(headerLinksBackgroundColorSetting)
const clearActiveTab = () => {
const activeLinks = document.querySelectorAll('.router-link')
for (const active of activeLinks) {
if (active.id) {
// Skip login and logoff buttons
if (active.id && active.id !== 'login' && active.id !== 'logoff') {
active.style.backgroundColor = 'transparent'
active.style.color = '#39455f'
}
@ -112,6 +113,12 @@ watchEffect(() => {
}
}
})
const getCurrentPath = () => {
const currentPath = route.path
return currentPath
}
</script>
<template>
@ -157,11 +164,11 @@ watchEffect(() => {
<arrow-down />
</el-icon>
</span>-->
<a v-bind:href="'/api/connect'" v-show="isShowLoginButton" class="login-button router-link">
<a v-bind:href="'/api/connect?redirect='+ encodeURIComponent(getCurrentPath())" v-show="isShowLoginButton" class="login-button router-link" id="login">
{{ $t('header.login') }}
</a>
<span v-show="isShowLogOffButton" class="login-user">{{ loginUsername }}</span>
<a v-bind:href="'/api/user/logoff'" v-show="isShowLogOffButton" class="logoff-button router-link">
<a v-bind:href="'/api/user/logoff?redirect=' + encodeURIComponent(getCurrentPath())" v-show="isShowLogOffButton" class="logoff-button router-link" id="logoff">
{{ $t('header.logoff') }}
</a>
</RouterView>
@ -227,8 +234,8 @@ nav {
cursor: pointer;
}
.login-button,
.logoff-button {
a.login-button,
a.logoff-button {
margin: 5px;
color: #ffffff;
background-color: #32b9ce;

View File

@ -264,10 +264,16 @@ const onError = (error) => {
<template>
<main>
<el-form ref="requestFormRef" :model="requestForm">
<el-form ref="requestFormRef" :model="requestForm" @submit.prevent>
<el-form-item prop="url">
<div class="flex-request-preview-panel">
<input type="text" v-model="url" :set="(requestForm.url = url)" id="search-input" />
<input
type="text"
v-model="url"
:set="(requestForm.url = url)"
id="search-input"
@keyup.enter="submit(requestFormRef, submitRequest)"
/>
<el-button
:type="type"
id="search-button"

View File

@ -37,6 +37,14 @@ export async function getOBPResourceDocs(apiStandardAndVersion: string): Promise
return await get(`/obp/${OBP_API_VERSION}/resource-docs/${apiStandardAndVersion}/obp`)
}
export async function getOBPDynamicResourceDocs(apiStandardAndVersion: string): Promise<any> {
const logMessage = `Loading Dynamic Docs for ${apiStandardAndVersion}`
console.log(logMessage)
updateLoadingInfoMessage(logMessage)
return await get(`/obp/${OBP_API_VERSION}/resource-docs/${apiStandardAndVersion}/obp?content=dynamic`)
}
export function getFilteredGroupedResourceDocs(apiStandardAndVersion: string, tags: any, docs: any): Promise<any> {
console.log(docs);
if (apiStandardAndVersion === undefined || docs === undefined || docs[apiStandardAndVersion] === undefined) return Promise.resolve<any>({})
@ -70,6 +78,20 @@ export async function cacheDoc(cacheStorageOfResourceDocs: any): Promise<any> {
const scannedAPIVersions = apiVersions.scanned_api_versions
const resourceDocsMapping: any = {}
for (const { apiStandard, API_VERSION } of scannedAPIVersions) {
// we need this to cache the dynamic entities resource doc
if (API_VERSION === 'dynamic-entity') {
const logMessage = `Caching Dynamic API { standard: ${apiStandard}, version: ${API_VERSION} }`
console.log(logMessage)
if (apiStandard) {
const version = `${apiStandard.toUpperCase()}${API_VERSION}`
const resourceDocs = await getOBPDynamicResourceDocs(version)
if (version && Object.keys(resourceDocs).includes('resource_docs'))
resourceDocsMapping[version] = resourceDocs
}
updateLoadingInfoMessage(logMessage)
continue
}
const logMessage = `Caching API { standard: ${apiStandard}, version: ${API_VERSION} }`
console.log(logMessage)
if (apiStandard) {

View File

@ -178,7 +178,7 @@ export const useChat = defineStore('chat', {
}
} catch (error) {
console.error('Error creating session:', error);
throw new Error(`Failed to create Opey session: ${error}`);
}
},

View File

@ -28,14 +28,26 @@
<script setup lang="ts">
import SearchNav from '../components/SearchNav.vue'
import Menu from '../components/Menu.vue'
import AutoLogout from '../components/AutoLogout.vue'
import ChatWidget from '../components/ChatWidget.vue'
import Collections from '../components/Collections.vue'
import { inject } from 'vue'
import { onMounted, ref } from 'vue'
import { getCurrentUser } from '../obp'
const isLoggedIn = ref(false);
onMounted(async () => {
const currentUser = await getCurrentUser()
const currentResponseKeys = Object.keys(currentUser)
isLoggedIn.value = currentResponseKeys.includes('username')
})
const isChatbotEnabled = import.meta.env.VITE_CHATBOT_ENABLED === 'true'
</script>
<template>
<AutoLogout v-if=isLoggedIn />
<el-container class="root">
<el-aside class="search-nav" width="20%">
<!--Left-->