Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Greenhouse] User activity tracker #290

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions apps/greenhouse/src/Shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import styles from "./styles.scss"
import StyleProvider from "juno-ui-components"
import useCommunication from "./hooks/useCommunication"
import { registerConsumer, currentState } from "url-state-provider"
import useIdleTimer from "./hooks/useIdleTimer"
import IdleTimerProvider from "./components/IdleTimerProvider"

/* IMPORTANT: Replace this with your app's name */
const URL_STATE_KEY = "greenhouse"
Expand Down Expand Up @@ -77,8 +79,8 @@ const Shell = (props = {}) => {

// Create query client which it can be used from overall in the app
const queryClient = new QueryClient()

useCommunication()
useIdleTimer({ timeout: 5 })

// INIT
// on app initial load save Endpoint and URL_STATE_KEY so it can be
Expand Down Expand Up @@ -131,15 +133,17 @@ const Shell = (props = {}) => {
return (
<Auth>
<QueryClientProvider client={queryClient}>
<ShellLayout>
{Object.keys(appsConfig).map((appName, i) => (
<App
name={appName}
key={i}
active={activeApps.indexOf(appName) >= 0}
/>
))}
</ShellLayout>
<IdleTimerProvider>
<ShellLayout>
{Object.keys(appsConfig).map((appName, i) => (
<App
name={appName}
key={i}
active={activeApps.indexOf(appName) >= 0}
/>
))}
</ShellLayout>
</IdleTimerProvider>
</QueryClientProvider>
</Auth>
)
Expand Down
41 changes: 41 additions & 0 deletions apps/greenhouse/src/components/IdleTimerProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { useState, useEffect } from "react"
import { Modal } from "juno-ui-components"
import useStore from "../hooks/useStore"

const IdleTimerProvider = ({ children }) => {
const userActivity = useStore((state) => state.userActivity)
const [showModal, setShowModal] = useState(false)

useEffect(() => {
if (!userActivity?.isActive) {
setShowModal(true)
}
}, [userActivity?.isActive])

useEffect(() => {
console.log("SHOW MODAL: ", showModal)
userActivity?.setShowInactiveModal(showModal)
}, [showModal])

return (
<>
{showModal && (
<Modal
open={true}
onCancel={() => {
setShowModal(false)
}}
cancelButtonLabel="Continue"
>
<p>
It seems you aren't anymore active. The workers are paused until you
hit continue.
</p>
</Modal>
)}
{children}
</>
)
}

export default IdleTimerProvider
12 changes: 12 additions & 0 deletions apps/greenhouse/src/hooks/useCommunication.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import useStore from "./useStore"

const useCommunication = () => {
const auth = useStore((state) => state.auth)
const userActivity = useStore((state) => state.userActivity)

useEffect(() => {
if (!auth.appLoaded || auth?.isProcessing || auth?.error) return
Expand All @@ -28,6 +29,17 @@ const useCommunication = () => {
if (unwatchUpdate) unwatchUpdate()
}
}, [auth?.setData, auth?.setAppLoaded])

useEffect(() => {
// check against undefined since this is a boolean
if (userActivity?.showInactiveModal === undefined) return

broadcast(
"GREENHOUSE_USER_ACTIVITY",
{ isActive: !userActivity?.showInactiveModal },
{ debug: true }
)
}, [userActivity?.showInactiveModal])
}

export default useCommunication
85 changes: 85 additions & 0 deletions apps/greenhouse/src/hooks/useIdleTimer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useEffect, useState, useMemo } from "react"
import useStore from "./useStore"

const DEFAULT_TIMEOUT = 1800 // 30 min

// TODO: should we return the counter?
const useIdleTimer = ({ timeout, onTimeout, onActive }) => {
const userActivity = useStore((state) => state.userActivity)
const [intervalChecker, setIntervalChecker] = useState(null)
const [counter, setCounter] = useState(0)
const [isActive, setIsActive] = useState(true) // default to true so it is not starting inactive

// set a default timeout
timeout = useMemo(() => {
if (!timeout) return DEFAULT_TIMEOUT
return timeout
}, [timeout])

// on load bind events and reset on timeout changes
useEffect(() => {
trackActivity()
startInterval()
return () => cleanUp()
}, [timeout])

// dispatch callbacks and save state into the store
useEffect(() => {
// save into the store
userActivity?.setIsActive(isActive)
// send callbacks
if (isActive) {
if (onActive) onActive()
} else {
if (onTimeout) onTimeout()
}
}, [isActive])

console.log("IDLETIMMER HOOK: ", isActive, counter)

// track user activity by adding event listeners
const trackActivity = () => {
window.addEventListener("mousemove", activity)
window.addEventListener("click", activity)
window.addEventListener("scroll", activity)
window.addEventListener("keydown", activity)
window.addEventListener("keydown", focus)
}

// cleanup all events
const cleanUp = () => {
clearInterval(intervalChecker)
window.removeEventListener("mousemove", activity)
window.removeEventListener("click", activity)
window.removeEventListener("scroll", activity)
window.removeEventListener("keydown", activity)
window.removeEventListener("focus", activity)
}

// set the expire time by reducing noise
const activity = () => {
setIsActive(true)
setCounter(0)
}

// check in regular periods if we still active
const startInterval = () => {
setIntervalChecker(
setInterval(() => {
// use functional updates since interval will be created once
// but we need to read the updated counter
setCounter((prevCounter) => {
const newCount = prevCounter + 1
if (newCount > timeout) {
setIsActive(false)
}
return newCount
})
}, 1000)
)
}

return { isActive: isActive, inActiveSince: counter }
}

export default useIdleTimer
20 changes: 20 additions & 0 deletions apps/greenhouse/src/hooks/useStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ const ACTIONS = {
SIGN_OUT: "signOut",
}

const createUserActivitySlice = (set, get) => ({
userActivity: {
// this state tracks the user activity
isActive: true,
setIsActive: (activity) => {
set((state) => ({
userActivity: { ...state.userActivity, isActive: activity },
}))
},
//this state tracks
showInactiveModal: false,
setShowInactiveModal: (activity) => {
set((state) => ({
userActivity: { ...state.userActivity, showInactiveModal: activity },
}))
},
},
})

const createAuthDataSlice = (set, get) => ({
auth: {
data: null,
Expand Down Expand Up @@ -70,6 +89,7 @@ const createAppsDataSlice = (set, get) => ({
const useStore = create((set, get) => ({
...createAuthDataSlice(set, get),
...createAppsDataSlice(set, get),
...createUserActivitySlice(set, get),
endpoint: "",
urlStateKey: "",
assetsHost: "",
Expand Down