Watcha admin re-design dashboard (#32)
* feat: extract Login from App component * refactor: remove obsolete package-lock.json since using yarn * feat: use matrix-js-sdk for login * feat: use URL() rather than string concatenation * fix: remove useless t() * feat: upgrade i18n engine * feat: add an error boundary * refactor: extensive but very safe cleaning of the code base No purpose changes : - format with Prettier - convert arrow methods with only an instruction into oneline - convert the setStates having only one argument into oneline - merge consecutive setStates - reorganize imports - improve react-bootstrap imports - convert small components into stateless functional components - isEmail() now returns only a boolean - convert React life cycles (componentDidMount...) to standard methods, not to class properties - remove useless constructors - cleaning of line breaks... * fix: replace obsolete prop for Alert components * feat: use classnames() to improve BooleanRow component * fix: upgrade dependencies * fix: restore homepage in package.json and update i18n config * feat: add context for matrix client * feat: replace deactivateAccount() with client.deactivateSynapseUser() * fix: add namespace array in i18n config * fix: turn off debug for i18n * fix: refresh the user table by validating user creation alert * feat: use context wherever token and baseUrl are needed * feat: add SuspenseFallback component * feat: delay fallback suspense display * cmd: yarn add react-table * refactor: replace object[xxx] with object.xxx * refactor: replace legacy functional components with arrow functions * cmd: yarn add restful-react * refactor: convert StatsTab into functional component * refactor: convert CardStats into functional component * feat: use restful-react for StatsTab component * feat: replace custom loading animation with a spinner * feat: replace App.css content with user.css one and improve centered layout * refactor: convert App into functional component * feat: add DelayedSpinner component * refactor: convert AdminHome into functional component * feat: improve variable names involved in changing tabs * fix: typo * feat: update i18n for tab titles * feat: update i18n for Login component * feat: improve pattern to avoid double submission * fix: pass the right prop to CardStat component for tab change * refactor: rename StatsTab component * refactor: convert Login into functional component * feat: add useMatrixClientContext() * feat: add TabReducerContext * feat: hide user list for empty rooms * fix: replace setState callback() with callback for deactivateSynapseUser() * feat: add ChangeTabContext * feat: add UsersTab & RoomsTab components * feat: add DispatchContext, UserIdContext and RoomIdContext * feat: use react-table for users tab * fix: improve layout * feat: add new user button * refactor: improve i18n hierarchy * feat: add the old CreateUserRightPanel to the new UsersTab * feat: add the old UserRightPanel to the new UsersTab * fix: limit the scope of css classes for user account status * fix: improve spinner layout * fix: restore missing i18n * fix: remove table outline * Watcha op313 modify user page on admin dashboard (#11) * feat: add email next to displayname on for each admin line. * style: deletion of underline on each admin line. * feat: deletion of useless console.log() * style: add new line at the end of AdminCardStats.js file. * style: format new code with prettier. * style: formatting with prettier (tabsize = 4). * feat: simplification of syntax code. * feat: calculate displayname before passing props. * Style: reorder props Co-authored-by: c-cal <57274151+c-cal@users.noreply.github.com> * Watcha op305 inconsistent values on admin stats (#10) * feat: assignement of rooms statistic values returned by synapse on dashboard. * style: translate new terms on room page on dashboard. * style: regroup oldstyle i18n * feat: fix of Warning: Encountered two children with the same key. Co-authored-by: c-cal <github-2c7c@zebrina.fr> * feat: add logged users informations on dashboard. * feat: add number of users with pending invitation on dashboard. * style: change 'member' terms by 'collaborator'. * feat: modification of item parameter in resolve function (with synapse modification of OP350). * feat: change watcha_room_list endpoint name (with synapse modification from OP320). * feat: add pendingInvitationUsers translation on dashboardTab.json. * feat: add admin translation on dashboardTab.json. * style: add usersPannel spacename. * style: add roomsPannel spacename. * Watcha admin release sprint 22 (#24) * feat: deletion of CardStats component. * feat: add some userPannel translation. * style: css for UserDashboardPanel component. * feat: creation of UsersDashboardPanel component. * feat: add UsersDashboardPanel on DashboardTab component. * feat: add some translation for roomsPannel. * feat: creation of RoomsDashboardPanel component. * feat: addition of RoomsDashboardPanel on dashboardTab. * feat: deletion of CardStats.js * feat: deletion of AdminCardStats.js. * feat: add noRoomsMessage on RoomsDashboardPanel. * fix: fix label value of pendingInvitationUsers. * style: add css for noRoomsMessage. * feat: modification of some translation on dashboardTab. * feat: addition of translation for serverStatePannel. * feat: creation of ServerStateDashboardPanel component. * feat: addition of serverStateDashboardPanel component on dashboardTab. * style: change syntax code. * feat: display empty string if no admin email value. * feat: addition of translation for tooltip messages. * feat: deletion of useless i18next on dashboardPanel component. * feat: addition of tooltip on usersPanelDasboard component. * style: modification of transaltion on dahsboardTab. * feat: deletion of useless react-18next import. * feat: deletion of useless Card component. * feat: replacement of card by fieldset and include list of admin in fieldset. * style: add some css modification. * style: spelling correction. * feat: addition of a key for Accordion component. * feat: addition of resfreshButton on dashboardTab (OP358). * feat: deletion of string formatting for date in serverPanel. * fix : change API endpoint name and add JSON content * fix: add RefreshButton component * fix: change API endpoint name for rooms list * fix: add ItemId parameter to resolve function. * fix: change 'member' role to 'collaborator' Co-authored-by: c-cal <github-2c7c@zebrina.fr> Co-authored-by: c-cal <57274151+c-cal@users.noreply.github.com>develop
parent
3ebdbdddf2
commit
3180ee1650
@ -1,10 +1,36 @@ |
||||
{ |
||||
"title": "Dashboard", |
||||
"usersPannel": { |
||||
"footerLink": "Administrate users" |
||||
"usersPanel": { |
||||
"collaborators": "Collaborators", |
||||
"partners": "Partners", |
||||
"administrators": "Administrators", |
||||
"loggedUsers": "At least once", |
||||
"monthlyUsers": "During last month", |
||||
"weeklyUsers": "Duringlast week", |
||||
"pendingInvitationUsers": "Pending invitations", |
||||
"administrateButton": "Administrate users", |
||||
"usersPerRole": "Roles of users", |
||||
"connectedUsers": "Connected users", |
||||
"otherStatistics": "Other statistics", |
||||
"administratorTooltip": "Users with access to the administration console of this instance.", |
||||
"collaboratorTooltip": "Internal users with the right to create rooms and invite users into them.", |
||||
"partnerTooltip": "External users who have been invited to one or more rooms by an internal user and have limited rights.", |
||||
"pendingInvitationTooltip": "Users who have been invited, but have not yet set their password or logged in." |
||||
}, |
||||
"roomsPannel": { |
||||
"footerLink": "Administrate rooms", |
||||
"noRoomsMessage": "This Watcha instance does not yet have some rooms. Return to Watcha to create a first room (button \"+\")." |
||||
"roomsPanel": { |
||||
"title": "Rooms per type", |
||||
"nonDirectRoomsCount": "Multiple conversations of which active", |
||||
"directRoomsCount": "Personnals conversations of which active", |
||||
"administrateButton": "Administrate rooms", |
||||
"noRoomsMessageOne": "This Watcha instance does not yet have some rooms.", |
||||
"noRoomsMessageTwo": "Return to Watcha to create a first room (button \"+\")." |
||||
}, |
||||
"serverStatePannel": { |
||||
"title": "Server state", |
||||
"generaleInformation": "General information", |
||||
"version": "Version", |
||||
"installDate": "Installed on", |
||||
"upgradeDate": "Upgrade on", |
||||
"diskUsage": "Disk space used" |
||||
} |
||||
} |
||||
} |
||||
|
||||
@ -1,10 +1,36 @@ |
||||
{ |
||||
"title": "Tableau de bord", |
||||
"usersPannel": { |
||||
"footerLink": "Administrer les utilisateurs" |
||||
"usersPanel": { |
||||
"collaborators": "Collaborateurs", |
||||
"partners": "Partenaires", |
||||
"administrators": "Administrateurs", |
||||
"loggedUsers": "Au moins une fois", |
||||
"monthlyUsers": "Sur le dernier mois", |
||||
"weeklyUsers": "Sur la dernière semaine", |
||||
"pendingInvitationUsers": "Invitations en attente", |
||||
"administrateButton": "Administrer les utilisateurs", |
||||
"usersPerRole": "Rôles des utilisateurs", |
||||
"connectedUsers": "Utilisateurs connectés", |
||||
"otherStatistics": "Autres statistiques", |
||||
"administratorTooltip": "Utilisateurs ayant accès à la console d'administration de l'instance.", |
||||
"collaboratorTooltip": "Utilisateurs internes ayant le droit de créer des salons et d'inviter des utilisateurs dans ces derniers.", |
||||
"partnerTooltip": "Utilisateurs externes ayant été invités dans un ou plusieurs salons, par un utilisateur interne et ayant des droits limités.", |
||||
"pendingInvitationTooltip": "Utilisateurs ayant été invités, mais n'ayant pas encore défini leur mot de passe, ni s'étant encore connecté." |
||||
}, |
||||
"roomsPannel": { |
||||
"footerLink": "Administrer les salons", |
||||
"noRoomsMessage": "Cette installation de Watcha n'a encore aucun salon. Retournez sur Watcha pour creer un premier Salon (bouton \"+\")" |
||||
"roomsPanel": { |
||||
"title": "Salons par type", |
||||
"nonDirectRoomsCount": "Conversations a plusieurs actives", |
||||
"directRoomsCount": "Conversations privées actives", |
||||
"administrateButton": "Administrer les salons", |
||||
"noRoomsMessageOne": "Cette installation de Watcha n'a encore aucun salon.", |
||||
"noRoomsMessageTwo": "Retournez sur Watcha pour creer un premier Salon (bouton \"+\")" |
||||
}, |
||||
"serverStatePannel": { |
||||
"title": "État du serveur", |
||||
"generaleInformation": "Informations générales", |
||||
"version": "Version", |
||||
"installDate": "Installé le", |
||||
"upgradeDate": "Mis à jour le", |
||||
"diskUsage": "Espace disque utilisé" |
||||
} |
||||
} |
||||
|
||||
@ -1,10 +0,0 @@ |
||||
import React from "react"; |
||||
|
||||
export default ({ onUserClicked, adminUserId, displayName, email }) => { |
||||
const onClick = () => onUserClicked(adminUserId); |
||||
return ( |
||||
<div className="AdminName" {...{ onClick }}> |
||||
{`${displayName} (${email})`} |
||||
</div> |
||||
); |
||||
}; |
||||
@ -1,87 +0,0 @@ |
||||
import React from "react"; |
||||
import { withTranslation } from "react-i18next"; |
||||
import Card from "react-bootstrap/Card"; |
||||
|
||||
import { useDispatchContext } from "./contexts"; |
||||
import AdminCardStats from "./AdminCardStats"; |
||||
|
||||
export default withTranslation()(({ title, tab, lines, t, footer }) => { |
||||
const dispatch = useDispatchContext(); |
||||
|
||||
const onCardClicked = () => dispatch({ tab }); |
||||
|
||||
const onUserClicked = userId => dispatch({ tab: "users", userId }); |
||||
|
||||
const getPanelContent = lines => { |
||||
const panelContent = []; |
||||
if(lines.length !== 0){ |
||||
for (const LINE in lines) { |
||||
if ({}.hasOwnProperty.call(lines, LINE)) { |
||||
if (lines[LINE].label === t("Admin")) { |
||||
const admins = []; |
||||
const profileInfosOfAdmins = lines[LINE].data; |
||||
for (const profileInfo of profileInfosOfAdmins) { |
||||
const displayName = setAdminName( |
||||
profileInfo["displayname"], |
||||
profileInfo["user_id"] |
||||
); |
||||
admins.push( |
||||
<div key={profileInfo["user_id"]}> |
||||
<AdminCardStats |
||||
email={profileInfo["email"]} |
||||
adminUserId={profileInfo["user_id"]} |
||||
{...{ onUserClicked, displayName }} |
||||
/> |
||||
</div> |
||||
); |
||||
} |
||||
panelContent.push( |
||||
<div key={lines[LINE].label}> |
||||
{" "} |
||||
{t("Administrators")}: {admins}{" "} |
||||
</div> |
||||
); |
||||
} else { |
||||
panelContent.push( |
||||
<div key={lines[LINE].label}> |
||||
{lines[LINE].label + ": " + lines[LINE].data} |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
} |
||||
}else{ |
||||
panelContent.push( |
||||
<div>{t("dashboardTab:roomsPannel.noRoomsMessage")}</div> |
||||
); |
||||
} |
||||
|
||||
return panelContent; |
||||
}; |
||||
|
||||
const setAdminName = (displayName, userId) => { |
||||
return displayName || userId.replace("@", "").split(":")[0]; |
||||
}; |
||||
|
||||
const PANEL_CONTENT = getPanelContent(lines); |
||||
|
||||
return ( |
||||
<Card className="statsPanel"> |
||||
<Card.Header> |
||||
<span> |
||||
{title} |
||||
</span> |
||||
</Card.Header> |
||||
<Card.Body> |
||||
<div className="statsPanelContent"> |
||||
<div>{PANEL_CONTENT}</div> |
||||
</div> |
||||
</Card.Body> |
||||
<Card.Footer> |
||||
<span className="statsPanelFooter" onClick={onCardClicked}> |
||||
{footer} |
||||
</span> |
||||
</Card.Footer> |
||||
</Card> |
||||
); |
||||
}); |
||||
@ -1,70 +1,79 @@ |
||||
import React from "react"; |
||||
import { useGet } from "restful-react"; |
||||
import { useTranslation } from "react-i18next"; |
||||
|
||||
import CardStats from "./CardStats"; |
||||
import React, { Component } from "react"; |
||||
import Col from "react-bootstrap/Col"; |
||||
import Row from "react-bootstrap/Col"; |
||||
import UsersDashboardPanel from "./UsersDashboardPanel"; |
||||
import RoomsDashboardPanel from "./RoomsDashboardPanel"; |
||||
import ServerStateDashboardPanel from "./ServerStateDashboardPanel"; |
||||
import CardDeck from "react-bootstrap/CardDeck"; |
||||
import DelayedSpinner from "./DelayedSpinner"; |
||||
import RefreshButton from "./RefreshButton"; |
||||
import { MatrixClientContext } from "./contexts"; |
||||
|
||||
class DashboardTab extends Component { |
||||
constructor(props) { |
||||
super(props); |
||||
this.state = { |
||||
apiAdress: "_matrix/client/r0/watcha_admin_stats", |
||||
datas: undefined, |
||||
loading: true, |
||||
}; |
||||
} |
||||
|
||||
static contextType = MatrixClientContext; |
||||
|
||||
export default () => { |
||||
const { t } = useTranslation(); |
||||
getDatas = async () => { |
||||
const client = this.context; |
||||
let datas; |
||||
try { |
||||
const adminStatsRequest = await fetch( |
||||
new URL(this.state.apiAdress, client.baseUrl), |
||||
{ |
||||
method: "GET", |
||||
headers: { |
||||
Authorization: "Bearer " + client.getAccessToken(), |
||||
}, |
||||
} |
||||
); |
||||
datas = JSON.parse(await adminStatsRequest.text()); |
||||
} catch (e) { |
||||
console.log("error: " + e); |
||||
return; |
||||
} |
||||
|
||||
const { data, loading, error } = useGet({ path: "watcha_admin_stats" }); |
||||
this.setState({ datas, loading: false }); |
||||
}; |
||||
|
||||
if (error) { |
||||
console.error(error); |
||||
componentDidMount() { |
||||
this.getDatas(); |
||||
} |
||||
|
||||
return loading || !data ? ( |
||||
<DelayedSpinner /> |
||||
) : ( |
||||
<div> |
||||
<div className="statsPanelsContainer"> |
||||
<CardStats |
||||
lines={[ |
||||
{ label: t("Members"), data: data.users.local }, |
||||
{ label: t("Partners"), data: data.users.partners }, |
||||
{ label: t("Admin"), data: data.admins }, |
||||
]} |
||||
title={t("usersTab:title")} |
||||
footer={t("dashboardTab:usersPannel.footerLink")} |
||||
tab="users" |
||||
/> |
||||
<CardStats |
||||
lines={ |
||||
data.rooms.non_direct_rooms_count === 0 && |
||||
data.rooms.direct_rooms_count === 0 |
||||
? [] |
||||
: [ |
||||
{ |
||||
label: t("Number of rooms"), |
||||
data: data.rooms.non_direct_rooms_count, |
||||
}, |
||||
{ |
||||
label: t("Of which active rooms"), |
||||
data: |
||||
data.rooms |
||||
.non_direct_active_rooms_count, |
||||
}, |
||||
{ |
||||
label: t( |
||||
"Number of personnals conversations" |
||||
), |
||||
data: data.rooms.direct_rooms_count, |
||||
}, |
||||
{ |
||||
label: t( |
||||
"Of which active personnals conversations" |
||||
), |
||||
data: |
||||
data.rooms.direct_active_rooms_count, |
||||
}, |
||||
] |
||||
} |
||||
title={t("roomsTab:title")} |
||||
footer={t("dashboardTab:roomsPannel.footerLink")} |
||||
tab="rooms" |
||||
/> |
||||
render() { |
||||
return this.state.loading || !this.state.datas ? ( |
||||
<DelayedSpinner /> |
||||
) : ( |
||||
<div> |
||||
<RefreshButton onClick={this.getDatas} variant="primary" /> |
||||
|
||||
<CardDeck className="dashboardPanelsContainer"> |
||||
<Col> |
||||
<RoomsDashboardPanel |
||||
datas={this.state.datas.rooms} |
||||
tab="rooms" |
||||
/> |
||||
<ServerStateDashboardPanel |
||||
datas={this.state.datas.server} |
||||
/> |
||||
</Col> |
||||
<Col> |
||||
<UsersDashboardPanel |
||||
datas={this.state.datas.users} |
||||
tab="users" |
||||
/> |
||||
</Col> |
||||
</CardDeck> |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
); |
||||
} |
||||
} |
||||
|
||||
export default DashboardTab; |
||||
|
||||
@ -0,0 +1,10 @@ |
||||
import React from "react"; |
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; |
||||
import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; |
||||
import Button from "react-bootstrap/Button"; |
||||
|
||||
export default ({ variant, onClick }) => ( |
||||
<Button className="refreshButton" {...{ variant, onClick }}> |
||||
<FontAwesomeIcon icon={faSyncAlt} /> |
||||
</Button> |
||||
); |
||||
@ -0,0 +1,67 @@ |
||||
import React from "react"; |
||||
import { withTranslation } from "react-i18next"; |
||||
import Card from "react-bootstrap/Card"; |
||||
import Table from "react-bootstrap/Table"; |
||||
import Row from "react-bootstrap/Row"; |
||||
import Container from "react-bootstrap/Container"; |
||||
import Button from "react-bootstrap/Button"; |
||||
import { useDispatchContext } from "./contexts"; |
||||
|
||||
export default withTranslation()(({ t, datas, tab }) => { |
||||
const dispatch = useDispatchContext(); |
||||
|
||||
const onAdministrateLinkClick = () => dispatch({ tab }); |
||||
|
||||
const contentPanel = |
||||
datas.non_direct_rooms_count === 0 && datas.direct_rooms_count === 0 ? ( |
||||
<div className="noRoomsMessage"> |
||||
{t("dashboardTab:roomsPanel.noRoomsMessageOne")} <br></br> |
||||
{t("dashboardTab:roomsPanel.noRoomsMessageTwo")} |
||||
</div> |
||||
) : ( |
||||
<Table> |
||||
<tbody> |
||||
<tr key={t("dashboardTab:roomsPanel.nonDirectRoomsCount")}> |
||||
<td className="sectionPanelLabel"> |
||||
{t("dashboardTab:roomsPanel.nonDirectRoomsCount")} |
||||
</td> |
||||
<td className="sectionPanelData"> |
||||
{`${datas.non_direct_active_rooms_count} / ${datas.non_direct_rooms_count}`} |
||||
</td> |
||||
</tr> |
||||
<tr key={t("dashboardTab:roomsPanel.directRoomsCount")}> |
||||
<td className="sectionPanelLabel"> |
||||
{t("dashboardTab:roomsPanel.directRoomsCount")} |
||||
</td> |
||||
<td className="sectionPanelData"> |
||||
{`${datas.direct_active_rooms_count} / ${datas.direct_rooms_count}`} |
||||
</td> |
||||
</tr> |
||||
</tbody> |
||||
</Table> |
||||
); |
||||
|
||||
return ( |
||||
<Card className="dashboardPanel"> |
||||
<Card.Header> |
||||
<span>{t("dashboardTab:roomsPanel.title")}</span> |
||||
<Button |
||||
className="dashboardAdministrateButton" |
||||
onClick={onAdministrateLinkClick} |
||||
> |
||||
{t("dashboardTab:roomsPanel.administrateButton")} |
||||
</Button> |
||||
</Card.Header> |
||||
<Card.Body> |
||||
<Container fluid> |
||||
<Row |
||||
className="dashboardPanelSection" |
||||
key={t("dashboardTab:roomsPanel.roomsPerType")} |
||||
> |
||||
{contentPanel} |
||||
</Row> |
||||
</Container> |
||||
</Card.Body> |
||||
</Card> |
||||
); |
||||
}); |
||||
@ -0,0 +1,87 @@ |
||||
import React from "react"; |
||||
import { withTranslation } from "react-i18next"; |
||||
import Card from "react-bootstrap/Card"; |
||||
import Table from "react-bootstrap/Table"; |
||||
import Row from "react-bootstrap/Row"; |
||||
import Container from "react-bootstrap/Container"; |
||||
|
||||
export default withTranslation()(({ t, datas }) => { |
||||
return ( |
||||
<Card className="dashboardPanel"> |
||||
<Card.Header> |
||||
<span>{t("dashboardTab:serverStatePannel:title")}</span> |
||||
</Card.Header> |
||||
<Card.Body> |
||||
<Container fluid> |
||||
<Row |
||||
className="dashboardPanelSection" |
||||
key={t("dashboardTab:serverStatePannel:title")} |
||||
> |
||||
<Table> |
||||
<tbody> |
||||
<tr |
||||
key={t( |
||||
"dashboardTab:serverStatePannel.version" |
||||
)} |
||||
> |
||||
<td className="sectionPanelLabel"> |
||||
{t( |
||||
"dashboardTab:serverStatePannel.version" |
||||
)} |
||||
</td> |
||||
<td className="sectionPanelData"> |
||||
{datas.watcha_release} |
||||
</td> |
||||
</tr> |
||||
<tr |
||||
key={t( |
||||
"dashboardTab:serverStatePannel.installDate" |
||||
)} |
||||
> |
||||
<td className="sectionPanelLabel"> |
||||
{t( |
||||
"dashboardTab:serverStatePannel.installDate" |
||||
)} |
||||
</td> |
||||
<td className="sectionPanelData"> |
||||
{datas.install_date} |
||||
</td> |
||||
</tr> |
||||
<tr |
||||
key={t( |
||||
"dashboardTab:serverStatePannel.upgradeDate" |
||||
)} |
||||
> |
||||
<td className="sectionPanelLabel"> |
||||
{t( |
||||
"dashboardTab:serverStatePannel.upgradeDate" |
||||
)} |
||||
</td> |
||||
<td className="sectionPanelData"> |
||||
{datas.upgrade_date} |
||||
</td> |
||||
</tr> |
||||
<tr |
||||
key={t( |
||||
"dashboardTab:serverStatePannel.diskUsage" |
||||
)} |
||||
> |
||||
<td className="sectionPanelLabel"> |
||||
{t( |
||||
"dashboardTab:serverStatePannel.diskUsage" |
||||
)} |
||||
</td> |
||||
<td className="sectionPanelData"> |
||||
{`${Math.round( |
||||
datas.disk.used / 1000000000 |
||||
)} Go (${datas.disk.percent}%)`}
|
||||
</td> |
||||
</tr> |
||||
</tbody> |
||||
</Table> |
||||
</Row> |
||||
</Container> |
||||
</Card.Body> |
||||
</Card> |
||||
); |
||||
}); |
||||
@ -0,0 +1,216 @@ |
||||
import React from "react"; |
||||
import { withTranslation } from "react-i18next"; |
||||
import Card from "react-bootstrap/Card"; |
||||
import Table from "react-bootstrap/Table"; |
||||
import Row from "react-bootstrap/Row"; |
||||
import Container from "react-bootstrap/Container"; |
||||
import Accordion from "react-bootstrap/Accordion"; |
||||
import Button from "react-bootstrap/Button"; |
||||
import OverlayTrigger from "react-bootstrap/OverlayTrigger"; |
||||
import Popover from "react-bootstrap/Popover"; |
||||
import { useDispatchContext } from "./contexts"; |
||||
import infoCircle from "./images/info-circle.svg"; |
||||
|
||||
export default withTranslation()(({ t, datas, tab }) => { |
||||
const standardSections = [ |
||||
{ |
||||
title: t("dashboardTab:usersPanel.usersPerRole"), |
||||
labels: [ |
||||
t("dashboardTab:usersPanel.administrators"), |
||||
t("dashboardTab:usersPanel.collaborators"), |
||||
t("dashboardTab:usersPanel.partners"), |
||||
], |
||||
datas: [ |
||||
datas.users_per_role.administrators, |
||||
datas.users_per_role.collaborators, |
||||
datas.users_per_role.partners, |
||||
], |
||||
}, |
||||
{ |
||||
title: t("dashboardTab:usersPanel.connectedUsers"), |
||||
labels: [ |
||||
t("dashboardTab:usersPanel.loggedUsers"), |
||||
t("dashboardTab:usersPanel.monthlyUsers"), |
||||
t("dashboardTab:usersPanel.weeklyUsers"), |
||||
], |
||||
datas: [ |
||||
datas.connected_users.number_of_users_logged_at_least_once, |
||||
datas.connected_users.number_of_last_month_logged_users, |
||||
datas.connected_users.number_of_last_week_logged_users, |
||||
], |
||||
}, |
||||
{ |
||||
title: t("dashboardTab:usersPanel.otherStatistics"), |
||||
labels: [t("dashboardTab:usersPanel.pendingInvitationUsers")], |
||||
datas: [ |
||||
datas.other_statistics.number_of_users_with_pending_invitation, |
||||
], |
||||
}, |
||||
]; |
||||
|
||||
const getInfoCircleTag = label => { |
||||
let tooltipMessage = ""; |
||||
switch (label) { |
||||
case t("dashboardTab:usersPanel.administrators"): |
||||
tooltipMessage = t( |
||||
"dashboardTab:usersPanel.administratorTooltip" |
||||
); |
||||
break; |
||||
case t("dashboardTab:usersPanel.collaborators"): |
||||
tooltipMessage = t( |
||||
"dashboardTab:usersPanel.collaboratorTooltip" |
||||
); |
||||
break; |
||||
case t("dashboardTab:usersPanel.partners"): |
||||
tooltipMessage = t("dashboardTab:usersPanel.partnerTooltip"); |
||||
break; |
||||
case t("dashboardTab:usersPanel.pendingInvitationUsers"): |
||||
tooltipMessage = t( |
||||
"dashboardTab:usersPanel.pendingInvitationTooltip" |
||||
); |
||||
break; |
||||
default: |
||||
return; |
||||
} |
||||
|
||||
const infoCircleTag = ( |
||||
<OverlayTrigger |
||||
overlay={ |
||||
<Popover className="tooltipMessage"> |
||||
<Popover.Content>{tooltipMessage}</Popover.Content> |
||||
</Popover> |
||||
} |
||||
placement="right" |
||||
> |
||||
<img src={infoCircle}></img> |
||||
</OverlayTrigger> |
||||
); |
||||
return infoCircleTag; |
||||
}; |
||||
|
||||
const dispatch = useDispatchContext(); |
||||
|
||||
const onAdministrateLinkClick = () => dispatch({ tab }); |
||||
|
||||
const onAdminUserClick = userId => dispatch({ tab: "users", userId }); |
||||
|
||||
const getStandardPanelContent = sections => { |
||||
const panelContent = []; |
||||
for (const index in sections) { |
||||
const section = sections[index]; |
||||
if (section.datas) { |
||||
const sectionContent = []; |
||||
for (const index in section.labels) { |
||||
const label = section.labels[index]; |
||||
const labelCell = |
||||
label === |
||||
t("dashboardTab:usersPanel.administrators") ? ( |
||||
<td className="sectionPanelAdminListLabel"> |
||||
{getAdminPanelContent( |
||||
datas.administrators_users |
||||
)} |
||||
</td> |
||||
) : ( |
||||
<td className="sectionPanelLabel"> |
||||
{label} {getInfoCircleTag(label)} |
||||
</td> |
||||
); |
||||
|
||||
sectionContent.push( |
||||
<tr key={label}> |
||||
{labelCell} |
||||
<td className="sectionPanelData"> |
||||
{section.datas[index]} |
||||
</td> |
||||
</tr> |
||||
); |
||||
} |
||||
if (sectionContent) { |
||||
panelContent.push( |
||||
<Row |
||||
className="dashboardPanelSection" |
||||
key={section.title} |
||||
> |
||||
<fieldset className="watcha-fieldset"> |
||||
<legend className="watcha-legend"> |
||||
{section.title} |
||||
</legend> |
||||
<Table> |
||||
<tbody>{sectionContent}</tbody> |
||||
</Table> |
||||
</fieldset> |
||||
</Row> |
||||
); |
||||
} |
||||
} |
||||
} |
||||
return panelContent; |
||||
}; |
||||
|
||||
const getAdminPanelContent = adminList => { |
||||
const adminPanelContent = []; |
||||
const sectionContent = []; |
||||
for (const index in adminList) { |
||||
const admin = adminList[index]; |
||||
sectionContent.push( |
||||
<tr key={admin.user_id}> |
||||
<td |
||||
className="adminUserRow" |
||||
onClick={() => onAdminUserClick(admin.user_id)} |
||||
> |
||||
{`${setAdminName(admin.displayname, admin.user_id)} ${ |
||||
admin.email ? `(${admin.email})` : "" |
||||
}`}
|
||||
</td> |
||||
</tr> |
||||
); |
||||
} |
||||
if (sectionContent) { |
||||
adminPanelContent.push( |
||||
<Accordion key={t("dashboardTab:usersPanel.administrators")} defaultActiveKey="0"> |
||||
<Card> |
||||
<Accordion.Toggle as={Card.Header} eventKey="1"> |
||||
{t("dashboardTab:usersPanel.administrators")}{" "} |
||||
{getInfoCircleTag( |
||||
t("dashboardTab:usersPanel.administrators") |
||||
)} |
||||
</Accordion.Toggle> |
||||
<Accordion.Collapse eventKey="1"> |
||||
<Card.Body> |
||||
{" "} |
||||
<Table> |
||||
<tbody>{sectionContent}</tbody> |
||||
</Table> |
||||
</Card.Body> |
||||
</Accordion.Collapse> |
||||
</Card> |
||||
</Accordion> |
||||
); |
||||
} |
||||
|
||||
return adminPanelContent; |
||||
}; |
||||
|
||||
const setAdminName = (displayname, adminUserID) => { |
||||
return displayname || adminUserID.replace("@", "").split(":")[0]; |
||||
}; |
||||
|
||||
const standardPanelContent = getStandardPanelContent(standardSections); |
||||
|
||||
return ( |
||||
<Card className="dashboardPanel"> |
||||
<Card.Header> |
||||
<span>{t("usersTab:title")}</span> |
||||
<Button |
||||
className="dashboardAdministrateButton" |
||||
onClick={onAdministrateLinkClick} |
||||
> |
||||
{t("dashboardTab:usersPanel.administrateButton")} |
||||
</Button> |
||||
</Card.Header> |
||||
<Card.Body> |
||||
<Container fluid>{standardPanelContent}</Container> |
||||
</Card.Body> |
||||
</Card> |
||||
); |
||||
}); |
||||
Loading…
Reference in new issue