604 lines
24 KiB
JavaScript
604 lines
24 KiB
JavaScript
import React from 'react';
|
|
import Card from '@mui/material/Card';
|
|
import CardContent from '@mui/material/CardContent';
|
|
import TabUnstyled from '@mui/base/TabUnstyled';
|
|
import TabsListUnstyled from '@mui/base/TabsListUnstyled';
|
|
import TabPanelUnstyled from '@mui/base/TabPanelUnstyled';
|
|
import TabsUnstyled from '@mui/base/TabsUnstyled';
|
|
import './App.css';
|
|
import './bbb-icons.css';
|
|
import {
|
|
FormattedMessage, FormattedDate, injectIntl, FormattedTime,
|
|
} from 'react-intl';
|
|
import { emojiConfigs } from './services/EmojiService';
|
|
import CardBody from './components/Card';
|
|
import UsersTable from './components/UsersTable';
|
|
import UserDetails from './components/UserDetails/component';
|
|
import { UserDetailsContext } from './components/UserDetails/context';
|
|
import StatusTable from './components/StatusTable';
|
|
import PollsTable from './components/PollsTable';
|
|
import ErrorMessage from './components/ErrorMessage';
|
|
import { makeUserCSVData, tsToHHmmss } from './services/UserService';
|
|
|
|
const TABS = {
|
|
OVERVIEW: 0,
|
|
OVERVIEW_ACTIVITY_SCORE: 1,
|
|
TIMELINE: 2,
|
|
POLLING: 3,
|
|
};
|
|
|
|
class App extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
loading: true,
|
|
invalidSessionCount: 0,
|
|
activitiesJson: {},
|
|
tab: 0,
|
|
meetingId: '',
|
|
learningDashboardAccessToken: '',
|
|
ldAccessTokenCopied: false,
|
|
sessionToken: '',
|
|
lastUpdated: null,
|
|
};
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.setDashboardParams(() => {
|
|
this.fetchActivitiesJson();
|
|
});
|
|
}
|
|
|
|
handleSaveSessionData(e) {
|
|
const { target: downloadButton } = e;
|
|
const { intl } = this.props;
|
|
const { activitiesJson } = this.state;
|
|
const {
|
|
name: meetingName, createdOn, users, polls,
|
|
} = activitiesJson;
|
|
const link = document.createElement('a');
|
|
const data = makeUserCSVData(users, polls, intl);
|
|
const filename = `LearningDashboard_${meetingName}_${new Date(createdOn).toISOString().substr(0, 10)}.csv`.replace(/ /g, '-');
|
|
|
|
downloadButton.setAttribute('disabled', 'true');
|
|
downloadButton.style.cursor = 'not-allowed';
|
|
link.setAttribute('href', `data:text/csv;charset=UTF-8,${encodeURIComponent(data)}`);
|
|
link.setAttribute('download', filename);
|
|
link.style.display = 'none';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
downloadButton.innerHTML = intl.formatMessage({ id: 'app.learningDashboard.sessionDataDownloadedLabel', defaultMessage: 'Downloaded!' });
|
|
setTimeout(() => {
|
|
downloadButton.innerHTML = intl.formatMessage({ id: 'app.learningDashboard.downloadSessionDataLabel', defaultMessage: 'Download Session Data' });
|
|
downloadButton.removeAttribute('disabled');
|
|
downloadButton.style.cursor = 'pointer';
|
|
downloadButton.focus();
|
|
}, 3000);
|
|
document.body.removeChild(link);
|
|
}
|
|
|
|
setDashboardParams(callback) {
|
|
let learningDashboardAccessToken = '';
|
|
let meetingId = '';
|
|
let sessionToken = '';
|
|
|
|
const urlSearchParams = new URLSearchParams(window.location.search);
|
|
const params = Object.fromEntries(urlSearchParams.entries());
|
|
|
|
if (typeof params.meeting !== 'undefined') {
|
|
meetingId = params.meeting;
|
|
}
|
|
|
|
if (typeof params.sessionToken !== 'undefined') {
|
|
sessionToken = params.sessionToken;
|
|
}
|
|
|
|
if (typeof params.report !== 'undefined') {
|
|
learningDashboardAccessToken = params.report;
|
|
} else {
|
|
const cookieName = `ld-${params.meeting}`;
|
|
const cDecoded = decodeURIComponent(document.cookie);
|
|
const cArr = cDecoded.split('; ');
|
|
cArr.forEach((val) => {
|
|
if (val.indexOf(`${cookieName}=`) === 0) {
|
|
learningDashboardAccessToken = val.substring((`${cookieName}=`).length);
|
|
}
|
|
});
|
|
|
|
// Extend AccessToken lifetime by 7d (in each access)
|
|
if (learningDashboardAccessToken !== '') {
|
|
const cookieExpiresDate = new Date();
|
|
cookieExpiresDate.setTime(cookieExpiresDate.getTime() + (3600000 * 24 * 7));
|
|
const value = `ld-${meetingId}=${learningDashboardAccessToken};`;
|
|
const expire = `expires=${cookieExpiresDate.toGMTString()};`;
|
|
const args = 'path=/;SameSite=None;Secure';
|
|
document.cookie = `${value} ${expire} ${args}`;
|
|
}
|
|
}
|
|
|
|
this.setState({ learningDashboardAccessToken, meetingId, sessionToken }, () => {
|
|
if (typeof callback === 'function') callback();
|
|
});
|
|
}
|
|
|
|
fetchMostUsedEmojis() {
|
|
const { activitiesJson } = this.state;
|
|
if (!activitiesJson) { return []; }
|
|
|
|
// Icon elements
|
|
const emojis = [...Object.keys(emojiConfigs)];
|
|
const icons = {};
|
|
emojis.forEach((emoji) => {
|
|
icons[emoji] = (<i className={`${emojiConfigs[emoji].icon} bbb-icon-card`} />);
|
|
});
|
|
|
|
// Count each emoji
|
|
const emojiCount = {};
|
|
emojis.forEach((emoji) => {
|
|
emojiCount[emoji] = 0;
|
|
});
|
|
const allEmojisUsed = Object
|
|
.values(activitiesJson.users || {})
|
|
.map((user) => user.emojis || [])
|
|
.flat(1);
|
|
allEmojisUsed.forEach((emoji) => {
|
|
emojiCount[emoji.name] += 1;
|
|
});
|
|
|
|
// Get the three most used
|
|
const mostUsedEmojis = Object
|
|
.entries(emojiCount)
|
|
.filter(([, count]) => count)
|
|
.sort(([, countA], [, countB]) => countA - countB)
|
|
.reverse()
|
|
.slice(0, 3);
|
|
return mostUsedEmojis.map(([emoji]) => icons[emoji]);
|
|
}
|
|
|
|
updateModalUser() {
|
|
const { user, dispatch, isOpen } = this.context;
|
|
const { activitiesJson } = this.state;
|
|
const { users } = activitiesJson;
|
|
|
|
if (isOpen && users[user.userKey]) {
|
|
dispatch({
|
|
type: 'changeUser',
|
|
user: users[user.userKey],
|
|
});
|
|
}
|
|
}
|
|
|
|
fetchActivitiesJson() {
|
|
const {
|
|
learningDashboardAccessToken, meetingId, sessionToken, invalidSessionCount,
|
|
} = this.state;
|
|
|
|
if (learningDashboardAccessToken !== '') {
|
|
fetch(`${meetingId}/${learningDashboardAccessToken}/learning_dashboard_data.json`)
|
|
.then((response) => response.json())
|
|
.then((json) => {
|
|
this.setState({
|
|
activitiesJson: json,
|
|
loading: false,
|
|
invalidSessionCount: 0,
|
|
lastUpdated: Date.now(),
|
|
});
|
|
this.updateModalUser();
|
|
}).catch(() => {
|
|
this.setState({ loading: false, invalidSessionCount: invalidSessionCount + 1 });
|
|
});
|
|
} else if (sessionToken !== '') {
|
|
const url = new URL('/bigbluebutton/api/learningDashboard', window.location);
|
|
fetch(`${url}?sessionToken=${sessionToken}`, { credentials: 'include' })
|
|
.then((response) => response.json())
|
|
.then((json) => {
|
|
if (json.response.returncode === 'SUCCESS') {
|
|
const jsonData = JSON.parse(json.response.data);
|
|
this.setState({
|
|
activitiesJson: jsonData,
|
|
loading: false,
|
|
invalidSessionCount: 0,
|
|
lastUpdated: Date.now(),
|
|
});
|
|
this.updateModalUser();
|
|
} else {
|
|
// When meeting is ended the sessionToken stop working, check for new cookies
|
|
this.setDashboardParams();
|
|
this.setState({ loading: false, invalidSessionCount: invalidSessionCount + 1 });
|
|
}
|
|
})
|
|
.catch(() => {
|
|
this.setState({ loading: false, invalidSessionCount: invalidSessionCount + 1 });
|
|
});
|
|
} else {
|
|
this.setState({ loading: false });
|
|
}
|
|
|
|
setTimeout(() => {
|
|
this.fetchActivitiesJson();
|
|
}, 10000 * (2 ** invalidSessionCount));
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
activitiesJson, tab, sessionToken, loading, lastUpdated,
|
|
learningDashboardAccessToken, ldAccessTokenCopied,
|
|
} = this.state;
|
|
const { intl } = this.props;
|
|
|
|
document.title = `${intl.formatMessage({ id: 'app.learningDashboard.bigbluebuttonTitle', defaultMessage: 'BigBlueButton' })} - ${intl.formatMessage({ id: 'app.learningDashboard.dashboardTitle', defaultMessage: 'Learning Analytics Dashboard' })} - ${activitiesJson.name}`;
|
|
|
|
function totalOfEmojis() {
|
|
if (activitiesJson && activitiesJson.users) {
|
|
return Object.values(activitiesJson.users)
|
|
.reduce((prevVal, elem) => prevVal + elem.emojis.length, 0);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function totalOfActivity() {
|
|
const usersTimes = Object.values(activitiesJson.users || {}).reduce((prev, user) => ([
|
|
...prev,
|
|
...Object.values(user.intIds),
|
|
]), []);
|
|
|
|
const minTime = Object.values(usersTimes || {}).reduce((prevVal, elem) => {
|
|
if (prevVal === 0 || elem.registeredOn < prevVal) return elem.registeredOn;
|
|
return prevVal;
|
|
}, 0);
|
|
|
|
const maxTime = Object.values(usersTimes || {}).reduce((prevVal, elem) => {
|
|
if (elem.leftOn === 0) return (new Date()).getTime();
|
|
if (elem.leftOn > prevVal) return elem.leftOn;
|
|
return prevVal;
|
|
}, 0);
|
|
|
|
return maxTime - minTime;
|
|
}
|
|
|
|
function getAverageActivityScore() {
|
|
let meetingAveragePoints = 0;
|
|
|
|
const allUsers = Object.values(activitiesJson.users || {})
|
|
.filter((currUser) => !currUser.isModerator);
|
|
const nrOfUsers = allUsers.length;
|
|
|
|
if (nrOfUsers === 0) return meetingAveragePoints;
|
|
|
|
// Calculate points of Talking
|
|
const usersTalkTime = allUsers.map((currUser) => currUser.talk.totalTime);
|
|
const maxTalkTime = Math.max(...usersTalkTime);
|
|
const totalTalkTime = usersTalkTime.reduce((prev, val) => prev + val, 0);
|
|
if (totalTalkTime > 0) {
|
|
meetingAveragePoints += ((totalTalkTime / nrOfUsers) / maxTalkTime) * 2;
|
|
}
|
|
|
|
// Calculate points of Chatting
|
|
const usersTotalOfMessages = allUsers.map((currUser) => currUser.totalOfMessages);
|
|
const maxMessages = Math.max(...usersTotalOfMessages);
|
|
const totalMessages = usersTotalOfMessages.reduce((prev, val) => prev + val, 0);
|
|
if (maxMessages > 0) {
|
|
meetingAveragePoints += ((totalMessages / nrOfUsers) / maxMessages) * 2;
|
|
}
|
|
|
|
// Calculate points of Raise hand
|
|
const usersRaiseHand = allUsers.map((currUser) => currUser.emojis.filter((emoji) => emoji.name === 'raiseHand').length);
|
|
const maxRaiseHand = Math.max(...usersRaiseHand);
|
|
const totalRaiseHand = usersRaiseHand.reduce((prev, val) => prev + val, 0);
|
|
if (maxRaiseHand > 0) {
|
|
meetingAveragePoints += ((totalRaiseHand / nrOfUsers) / maxRaiseHand) * 2;
|
|
}
|
|
|
|
// Calculate points of Emojis
|
|
const usersEmojis = allUsers.map((currUser) => currUser.emojis.filter((emoji) => emoji.name !== 'raiseHand').length);
|
|
const maxEmojis = Math.max(...usersEmojis);
|
|
const totalEmojis = usersEmojis.reduce((prev, val) => prev + val, 0);
|
|
if (maxEmojis > 0) {
|
|
meetingAveragePoints += ((totalEmojis / nrOfUsers) / maxEmojis) * 2;
|
|
}
|
|
|
|
// Calculate points of Polls
|
|
const totalOfPolls = Object.values(activitiesJson.polls || {}).length;
|
|
if (totalOfPolls > 0) {
|
|
const totalAnswers = allUsers
|
|
.reduce((prevVal, currUser) => prevVal + Object.values(currUser.answers || {}).length, 0);
|
|
meetingAveragePoints += ((totalAnswers / nrOfUsers) / totalOfPolls) * 2;
|
|
}
|
|
|
|
return meetingAveragePoints;
|
|
}
|
|
|
|
function getErrorMessage() {
|
|
if (learningDashboardAccessToken === '' && sessionToken === '') {
|
|
return intl.formatMessage({ id: 'app.learningDashboard.errors.invalidToken', defaultMessage: 'Invalid session token' });
|
|
}
|
|
|
|
if (activitiesJson === {} || typeof activitiesJson.name === 'undefined') {
|
|
return intl.formatMessage({ id: 'app.learningDashboard.errors.dataUnavailable', defaultMessage: 'Data is no longer available' });
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
if (loading === false && getErrorMessage() !== '') return <ErrorMessage message={getErrorMessage()} />;
|
|
|
|
const usersCount = Object.values(activitiesJson.users || {})
|
|
.filter((u) => activitiesJson.endedOn > 0
|
|
|| Object.values(u.intIds)[Object.values(u.intIds).length - 1].leftOn === 0)
|
|
.length;
|
|
|
|
return (
|
|
<div className="mx-10">
|
|
<div className="flex flex-col sm:flex-row items-start justify-between pb-3">
|
|
<h1 className="mt-3 text-2xl font-semibold whitespace-nowrap inline-block">
|
|
<FormattedMessage id="app.learningDashboard.dashboardTitle" defaultMessage="Learning Dashboard" />
|
|
{
|
|
ldAccessTokenCopied === true
|
|
? (
|
|
<span className="text-xs text-gray-500 font-normal ml-2">
|
|
<FormattedMessage id="app.learningDashboard.linkCopied" defaultMessage="Link successfully copied!" />
|
|
</span>
|
|
)
|
|
: null
|
|
}
|
|
<br />
|
|
<span className="text-sm font-medium">{activitiesJson.name || ''}</span>
|
|
</h1>
|
|
<div className="mt-3 col-text-right py-1 text-gray-500 inline-block">
|
|
<p className="font-bold">
|
|
<div className="inline" data-test="meetingDateDashboard">
|
|
<FormattedDate
|
|
value={activitiesJson.createdOn}
|
|
year="numeric"
|
|
month="short"
|
|
day="numeric"
|
|
/>
|
|
</div>
|
|
|
|
{
|
|
activitiesJson.endedOn > 0
|
|
? (
|
|
<span className="px-2 py-1 font-semibold leading-tight text-red-700 bg-red-100 rounded-full">
|
|
<FormattedMessage id="app.learningDashboard.indicators.meetingStatusEnded" defaultMessage="Ended" data-test="meetingStatusEndedDashboard" />
|
|
</span>
|
|
)
|
|
: null
|
|
}
|
|
{
|
|
activitiesJson.endedOn === 0
|
|
? (
|
|
<span className="px-2 py-1 font-semibold leading-tight text-green-700 bg-green-100 rounded-full" data-test="meetingStatusActiveDashboard">
|
|
<FormattedMessage id="app.learningDashboard.indicators.meetingStatusActive" defaultMessage="Active" />
|
|
</span>
|
|
)
|
|
: null
|
|
}
|
|
</p>
|
|
<p data-test="meetingDurationTimeDashboard">
|
|
<FormattedMessage id="app.learningDashboard.indicators.duration" defaultMessage="Duration" />
|
|
:
|
|
{tsToHHmmss(totalOfActivity())}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<TabsUnstyled
|
|
defaultValue={0}
|
|
onChange={(e, v) => {
|
|
this.setState({ tab: v });
|
|
}}
|
|
>
|
|
<TabsListUnstyled className="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
|
<TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-pink-500 ring-offset-2" data-test="activeUsersPanelDashboard">
|
|
<Card>
|
|
<CardContent classes={{ root: '!p-0' }}>
|
|
<CardBody
|
|
name={
|
|
activitiesJson.endedOn === 0
|
|
? intl.formatMessage({ id: 'app.learningDashboard.indicators.usersOnline', defaultMessage: 'Active Users' })
|
|
: intl.formatMessage({ id: 'app.learningDashboard.indicators.usersTotal', defaultMessage: 'Total Number Of Users' })
|
|
}
|
|
number={usersCount}
|
|
cardClass={tab === TABS.OVERVIEW ? 'border-pink-500' : 'hover:border-pink-500 border-white'}
|
|
iconClass="bg-pink-50 text-pink-500"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className="h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2"
|
|
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
|
/>
|
|
</svg>
|
|
</CardBody>
|
|
</CardContent>
|
|
</Card>
|
|
</TabUnstyled>
|
|
<TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-green-500 ring-offset-2" data-test="activityScorePanelDashboard">
|
|
<Card>
|
|
<CardContent classes={{ root: '!p-0' }}>
|
|
<CardBody
|
|
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.activityScore', defaultMessage: 'Activity Score' })}
|
|
number={intl.formatNumber((getAverageActivityScore() || 0), {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 1,
|
|
})}
|
|
cardClass={tab === TABS.OVERVIEW_ACTIVITY_SCORE ? 'border-green-500' : 'hover:border-green-500 border-white'}
|
|
iconClass="bg-green-200 text-green-700"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className="h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2"
|
|
d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z"
|
|
/>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2"
|
|
d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z"
|
|
/>
|
|
</svg>
|
|
</CardBody>
|
|
</CardContent>
|
|
</Card>
|
|
</TabUnstyled>
|
|
<TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-purple-500 ring-offset-2" data-test="timelinePanelDashboard">
|
|
<Card>
|
|
<CardContent classes={{ root: '!p-0' }}>
|
|
<CardBody
|
|
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.timeline', defaultMessage: 'Timeline' })}
|
|
number={totalOfEmojis()}
|
|
cardClass={tab === TABS.TIMELINE ? 'border-purple-500' : 'hover:border-purple-500 border-white'}
|
|
iconClass="bg-purple-200 text-purple-500"
|
|
>
|
|
{this.fetchMostUsedEmojis()}
|
|
</CardBody>
|
|
</CardContent>
|
|
</Card>
|
|
</TabUnstyled>
|
|
<TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-blue-500 ring-offset-2" data-test="pollsPanelDashboard">
|
|
<Card>
|
|
<CardContent classes={{ root: '!p-0' }}>
|
|
<CardBody
|
|
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.polls', defaultMessage: 'Polls' })}
|
|
number={Object.values(activitiesJson.polls || {}).length}
|
|
cardClass={tab === TABS.POLLING ? 'border-blue-500' : 'hover:border-blue-500 border-white'}
|
|
iconClass="bg-blue-100 text-blue-500"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className="h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2"
|
|
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
|
|
/>
|
|
</svg>
|
|
</CardBody>
|
|
</CardContent>
|
|
</Card>
|
|
</TabUnstyled>
|
|
</TabsListUnstyled>
|
|
<TabPanelUnstyled value={0}>
|
|
<h2 className="block my-2 pr-2 text-xl font-semibold">
|
|
<FormattedMessage id="app.learningDashboard.usersTable.title" defaultMessage="Overview" />
|
|
</h2>
|
|
<div className="w-full overflow-hidden rounded-md shadow-xs border-2 border-gray-100">
|
|
<div className="w-full overflow-x-auto">
|
|
<UsersTable
|
|
allUsers={activitiesJson.users}
|
|
totalOfActivityTime={totalOfActivity()}
|
|
totalOfPolls={Object.values(activitiesJson.polls || {}).length}
|
|
tab="overview"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</TabPanelUnstyled>
|
|
<TabPanelUnstyled value={1}>
|
|
<h2 className="block my-2 pr-2 text-xl font-semibold">
|
|
<FormattedMessage id="app.learningDashboard.usersTable.title" defaultMessage="Overview" />
|
|
</h2>
|
|
<div className="w-full overflow-hidden rounded-md shadow-xs border-2 border-gray-100">
|
|
<div className="w-full overflow-x-auto">
|
|
<UsersTable
|
|
allUsers={activitiesJson.users}
|
|
totalOfActivityTime={totalOfActivity()}
|
|
totalOfPolls={Object.values(activitiesJson.polls || {}).length}
|
|
tab="overview_activityscore"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</TabPanelUnstyled>
|
|
<TabPanelUnstyled value={2}>
|
|
<h2 className="block my-2 pr-2 text-xl font-semibold">
|
|
<FormattedMessage id="app.learningDashboard.statusTimelineTable.title" defaultMessage="Timeline" />
|
|
</h2>
|
|
<div className="w-full overflow-hidden rounded-md shadow-xs border-2 border-gray-100">
|
|
<div className="w-full overflow-x-auto">
|
|
<StatusTable
|
|
allUsers={activitiesJson.users}
|
|
slides={activitiesJson.presentationSlides}
|
|
meetingId={activitiesJson.intId}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</TabPanelUnstyled>
|
|
<TabPanelUnstyled value={3}>
|
|
<h2 className="block my-2 pr-2 text-xl font-semibold">
|
|
<FormattedMessage id="app.learningDashboard.pollsTable.title" defaultMessage="Polls" />
|
|
</h2>
|
|
<div className="w-full overflow-hidden rounded-md shadow-xs border-2 border-gray-100">
|
|
<div className="w-full overflow-x-auto">
|
|
<PollsTable polls={activitiesJson.polls} allUsers={activitiesJson.users} />
|
|
</div>
|
|
</div>
|
|
</TabPanelUnstyled>
|
|
</TabsUnstyled>
|
|
<UserDetails dataJson={activitiesJson} />
|
|
<hr className="my-8" />
|
|
<div className="flex justify-between pb-8 text-xs text-gray-800 dark:text-gray-400 whitespace-nowrap flex-col sm:flex-row">
|
|
<div className="flex flex-col justify-center mb-4 sm:mb-0">
|
|
<p className="text-gray-700">
|
|
{
|
|
lastUpdated && (
|
|
<>
|
|
<FormattedMessage
|
|
id="app.learningDashboard.lastUpdatedLabel"
|
|
defaultMessage="Last updated at"
|
|
/>
|
|
|
|
<FormattedTime
|
|
value={lastUpdated}
|
|
/>
|
|
|
|
<FormattedDate
|
|
value={lastUpdated}
|
|
year="numeric"
|
|
month="long"
|
|
day="numeric"
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
</p>
|
|
</div>
|
|
<button
|
|
data-test="downloadSessionDataDashboard"
|
|
type="button"
|
|
className="border-2 text-gray-700 border-gray-200 rounded-md px-4 py-2 bg-white focus:outline-none focus:ring ring-offset-2 focus:ring-gray-500 focus:ring-opacity-50"
|
|
onClick={this.handleSaveSessionData.bind(this)}
|
|
>
|
|
<FormattedMessage
|
|
id="app.learningDashboard.downloadSessionDataLabel"
|
|
defaultMessage="Download Session Data"
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
App.contextType = UserDetailsContext;
|
|
|
|
export default injectIntl(App);
|