refactor: appease the linter

develop
c-cal 4 years ago
parent 0cf491deee
commit c57e807904
  1. 21
      .eslintrc.yaml
  2. 3
      public/locales/en/usersTab.json
  3. 3
      public/locales/fr/usersTab.json
  4. 0
      src/AdminHome.jsx
  5. 19
      src/AdministratorList.jsx
  6. 12
      src/Alert.jsx
  7. 85
      src/App.jsx
  8. 18
      src/ApplicationDashboardPanel.jsx
  9. 3
      src/DashboardTab.jsx
  10. 10
      src/Date.jsx
  11. 0
      src/DelayedSpinner.jsx
  12. 28
      src/ErrorBoundary.jsx
  13. 12
      src/HeaderTooltip.jsx
  14. 18
      src/ItemTable.jsx
  15. 10
      src/LabelTooltip.jsx
  16. 37
      src/Login.jsx
  17. 12
      src/NewItemButton.jsx
  18. 24
      src/NewItemModal.jsx
  19. 32
      src/NewUserForm.jsx
  20. 66
      src/NewUserModal.js
  21. 91
      src/NewUserModal.jsx
  22. 10
      src/PanelRow.js
  23. 22
      src/PanelRow.jsx
  24. 10
      src/RoomPermalink.jsx
  25. 21
      src/RoomsDashboardPanel.jsx
  26. 34
      src/RoomsTab.jsx
  27. 12
      src/SearchBox.jsx
  28. 5
      src/Status.js
  29. 13
      src/Status.jsx
  30. 14
      src/StorageManager.js
  31. 51
      src/TableTab.jsx
  32. 40
      src/UsersDashboardPanel.jsx
  33. 50
      src/UsersTab.jsx
  34. 13
      src/i18n.js
  35. 0
      src/index.jsx

@ -2,3 +2,24 @@ extends:
- airbnb - airbnb
- airbnb/hooks - airbnb/hooks
- prettier - prettier
rules:
no-shadow:
- warn
no-param-reassign:
- warn
jsx-a11y/click-events-have-key-events:
- warn
jsx-a11y/no-static-element-interactions:
- warn
react-hooks/exhaustive-deps:
- warn
react/jsx-props-no-spreading:
- warn
- html: ignore
explicitSpread: ignore
react/jsx-filename-extension:
- error
- extensions:
- .js
- .jsx
- .tsx

@ -18,8 +18,7 @@
"displayName": "Display name", "displayName": "Display name",
"emailAddress": "Email address", "emailAddress": "Email address",
"lastSeen": "Last activity on", "lastSeen": "Last activity on",
"role": "Role", "role": "Role"
"status": "Status"
}, },
"buttonTooltip": "Create a new user", "buttonTooltip": "Create a new user",
"roleHeaderTooltip": { "roleHeaderTooltip": {

@ -18,8 +18,7 @@
"displayName": "Nom affiché", "displayName": "Nom affiché",
"emailAddress": "Adresse électronique", "emailAddress": "Adresse électronique",
"lastSeen": "Dernière activité le", "lastSeen": "Dernière activité le",
"role": "Rôle", "role": "Rôle"
"status": "Statut"
}, },
"buttonTooltip": "Créer un nouvel utilisateur", "buttonTooltip": "Créer un nouvel utilisateur",
"statusHeaderTooltip": { "statusHeaderTooltip": {

@ -1,13 +1,16 @@
import React from "react"; import React from "react";
import { useDispatchContext } from "./contexts"; import PropTypes from "prop-types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Accordion from "react-bootstrap/Accordion"; import Accordion from "react-bootstrap/Accordion";
import Card from "react-bootstrap/Card"; import Card from "react-bootstrap/Card";
import { useDispatchContext } from "./contexts";
import icon from "./images/expand-button.svg"; import icon from "./images/expand-button.svg";
import "./css/AdministratorsList.scss"; import "./css/AdministratorsList.scss";
export default ({ administratorList }) => { const AdministratorList = ({ administratorList }) => {
const { t } = useTranslation("dashboardTab"); const { t } = useTranslation("dashboardTab");
const dispatch = useDispatchContext(); const dispatch = useDispatchContext();
@ -39,3 +42,15 @@ export default ({ administratorList }) => {
</Accordion> </Accordion>
); );
}; };
AdministratorList.propTypes = {
administratorList: PropTypes.arrayOf(
PropTypes.shape({
user_id: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
displayname: PropTypes.string.isRequired,
})
).isRequired,
};
export default AdministratorList;

@ -1,9 +1,11 @@
import React from "react"; import React from "react";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Alert from "react-bootstrap/Alert"; import Alert from "react-bootstrap/Alert";
import Button from "react-bootstrap/Button"; import Button from "react-bootstrap/Button";
export default ({ variant, message, onClick }) => { const CustomAlert = ({ variant, message, onClick }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Alert className="flex-fill" variant={variant}> <Alert className="flex-fill" variant={variant}>
@ -16,3 +18,11 @@ export default ({ variant, message, onClick }) => {
</Alert> </Alert>
); );
}; };
CustomAlert.propTypes = {
variant: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired
};
export default CustomAlert;

@ -16,39 +16,15 @@ export default () => {
const [clientPrepared, setClientPrepared] = useState(false); const [clientPrepared, setClientPrepared] = useState(false);
const [missingAccessToken, setMissingAccessToken] = useState(false); const [missingAccessToken, setMissingAccessToken] = useState(false);
useEffect(() => {
Promise.resolve(getHsBaseUrl()).then(baseUrl => {
let client;
StorageManager.idbLoad("account", "mx_access_token").then(accessToken => {
if (!accessToken) {
accessToken = localStorage.getItem("mx_access_token");
}
const userId = localStorage.getItem("mx_user_id");
if (accessToken && userId) {
client = sdk.createClient({
baseUrl,
accessToken,
userId,
});
setupClient(client);
} else {
client = sdk.createClient({ baseUrl });
setMissingAccessToken(true);
}
setClient(client);
});
});
}, []);
const getHsBaseUrl = () => const getHsBaseUrl = () =>
localStorage.getItem("mx_hs_url") || localStorage.getItem("mx_hs_url") ||
fetch("/.well-known/matrix/client") fetch("/.well-known/matrix/client")
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
console.log(`.well-known discovery: ${data}`); console.log(`.well-known discovery: ${data}`);
return data["m.homeserver"]["base_url"]; return data["m.homeserver"].base_url;
}) })
.catch(error => { .catch(() => {
const hsBaseUrl = process.env.HS_URL || "http://localhost:8008"; const hsBaseUrl = process.env.HS_URL || "http://localhost:8008";
console.log(`Set ${hsBaseUrl} as home server url`); console.log(`Set ${hsBaseUrl} as home server url`);
return hsBaseUrl; return hsBaseUrl;
@ -56,7 +32,7 @@ export default () => {
const setupClient = async client => { const setupClient = async client => {
await client.startClient({ initialSyncLimit: 10 }); await client.startClient({ initialSyncLimit: 10 });
client.on("sync", (state, prevState, response) => { client.on("sync", (state, prevState) => {
if (state === "SYNCING" && prevState === "SYNCING") { if (state === "SYNCING" && prevState === "SYNCING") {
return; return;
} }
@ -75,6 +51,30 @@ export default () => {
}); });
}; };
useEffect(() => {
Promise.resolve(getHsBaseUrl()).then(baseUrl => {
let client;
StorageManager.idbLoad("account", "mx_access_token").then(accessToken => {
if (!accessToken) {
accessToken = localStorage.getItem("mx_access_token");
}
const userId = localStorage.getItem("mx_user_id");
if (accessToken && userId) {
client = sdk.createClient({
baseUrl,
accessToken,
userId,
});
setupClient(client);
} else {
client = sdk.createClient({ baseUrl });
setMissingAccessToken(true);
}
setClient(client);
});
});
}, []);
const getRestfulConfig = () => ({ const getRestfulConfig = () => ({
base: new URL("_matrix/client/r0", client.baseUrl).href, base: new URL("_matrix/client/r0", client.baseUrl).href,
requestOptions: { requestOptions: {
@ -82,24 +82,29 @@ export default () => {
Authorization: `Bearer ${client.getAccessToken()}`, Authorization: `Bearer ${client.getAccessToken()}`,
}, },
}, },
onError: (error, retry, repsonse) => { onError: (error, retry, response) => {
repsonse && console.error(repsonse); if (response) {
console.error(response);
}
}, },
}); });
let view;
if (clientPrepared) {
view = (
<RestfulProvider {...getRestfulConfig()}>
<AdminHome />
</RestfulProvider>
);
} else if (missingAccessToken) {
view = <Login {...{ setupClient }} />;
} else {
view = <DelayedSpinner />;
}
return ( return (
<Suspense fallback={<DelayedSpinner />}> <Suspense fallback={<DelayedSpinner />}>
<MatrixClientContext.Provider value={client}> <MatrixClientContext.Provider value={client}>{view}</MatrixClientContext.Provider>
{clientPrepared ? (
<RestfulProvider {...getRestfulConfig()}>
<AdminHome />
</RestfulProvider>
) : missingAccessToken ? (
<Login {...{ setupClient }} />
) : (
<DelayedSpinner />
)}
</MatrixClientContext.Provider>
</Suspense> </Suspense>
); );
}; };

@ -1,4 +1,6 @@
import React from "react"; import React from "react";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Card from "react-bootstrap/Card"; import Card from "react-bootstrap/Card";
@ -6,9 +8,10 @@ import PanelRow from "./PanelRow";
import "./css/DashboardPanel.scss"; import "./css/DashboardPanel.scss";
export default ({ applicationMetrics }) => { const ApplicationMetrics = ({ applicationMetrics }) => {
const { t } = useTranslation("dashboardTab"); const { t } = useTranslation("dashboardTab");
// eslint-disable-next-line camelcase
const { watcha_release, install_date, upgrade_date, disk_usage } = applicationMetrics; const { watcha_release, install_date, upgrade_date, disk_usage } = applicationMetrics;
return ( return (
@ -17,11 +20,24 @@ export default ({ applicationMetrics }) => {
<span>{t("applicationPanel.title")}</span> <span>{t("applicationPanel.title")}</span>
</Card.Header> </Card.Header>
<Card.Body className="DashboardPanel_body"> <Card.Body className="DashboardPanel_body">
{/* eslint-disable camelcase */}
<PanelRow label={t("applicationPanel.version")} value={watcha_release} /> <PanelRow label={t("applicationPanel.version")} value={watcha_release} />
<PanelRow label={t("applicationPanel.installDate")} value={install_date} /> <PanelRow label={t("applicationPanel.installDate")} value={install_date} />
<PanelRow label={t("applicationPanel.upgradeDate")} value={upgrade_date} /> <PanelRow label={t("applicationPanel.upgradeDate")} value={upgrade_date} />
<PanelRow label={t("applicationPanel.diskUsage")} value={disk_usage} /> <PanelRow label={t("applicationPanel.diskUsage")} value={disk_usage} />
{/* eslint-enable camelcase */}
</Card.Body> </Card.Body>
</Card> </Card>
); );
}; };
ApplicationMetrics.propTypes = {
applicationMetrics: PropTypes.shape({
disk_usage: PropTypes.string,
watcha_release: PropTypes.string,
upgrade_date: PropTypes.string,
install_date: PropTypes.string,
}).isRequired,
};
export default ApplicationMetrics;

@ -1,4 +1,5 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import { useGet } from "restful-react"; import { useGet } from "restful-react";
import CardDeck from "react-bootstrap/CardDeck"; import CardDeck from "react-bootstrap/CardDeck";
import Col from "react-bootstrap/Col"; import Col from "react-bootstrap/Col";
@ -31,7 +32,7 @@ export default () => {
useEffect(() => { useEffect(() => {
setMetrics(data); setMetrics(data);
loading && setLoading(false); setLoading(false);
if (intervalIdRef.current) { if (intervalIdRef.current) {
clearInterval(intervalIdRef.current); clearInterval(intervalIdRef.current);
} }

@ -1,9 +1,17 @@
import React from "react"; import React from "react";
import PropTypes from "prop-types";
import moment from "moment"; import moment from "moment";
export default ({ timestamp }) => { const Date = ({ timestamp }) => {
const m = moment(timestamp); const m = moment(timestamp);
const shortDate = m.format("L"); const shortDate = m.format("L");
const fullDate = m.format("LLLL"); const fullDate = m.format("LLLL");
return <span title={fullDate}>{shortDate}</span>; return <span title={fullDate}>{shortDate}</span>;
}; };
Date.propTypes = {
timestamp: PropTypes.number.isRequired,
};
export default Date;

@ -1,5 +1,8 @@
import React, { Component } from "react"; import React, { Component } from "react";
import PropTypes from "prop-types";
import Container from "react-bootstrap/Container"; import Container from "react-bootstrap/Container";
import DelayedSpinner from "./DelayedSpinner";
export default class ErrorBoundary extends Component { export default class ErrorBoundary extends Component {
constructor(props) { constructor(props) {
@ -7,28 +10,39 @@ export default class ErrorBoundary extends Component {
this.state = { hasError: false }; this.state = { hasError: false };
} }
static getDerivedStateFromError(error) { static getDerivedStateFromError() {
return { hasError: true }; return { hasError: true };
} }
render() { render() {
const { hasError } = this.state;
const { children } = this.props;
const email = "contact@watcha.fr"; const email = "contact@watcha.fr";
const redirectUrl = window.location.host; const redirectUrl = window.location.host;
return this.state.hasError ? (
return hasError ? (
<Container className="fullCentered"> <Container className="fullCentered">
<h1 className="mb-5">Something went wrong!</h1> <h1 className="mb-5">Something went wrong!</h1>
<p> <p>
{"You can try to refresh the page, or follow this link "} You can try to refresh the page, or follow this link <a href="/">{redirectUrl}</a> to return to
<a href={"/"}>{redirectUrl}</a> Watcha.
{" to return to Watcha."}
</p> </p>
<p> <p>
{"Should the failure happen again, please contact us at "} {"Should the failure happen again, please contact us at "}
<a href={"mailto:" + email}>{email}</a>. <a href={`mailto:${email}`}>{email}</a>.
</p> </p>
</Container> </Container>
) : ( ) : (
this.props.children children
); );
} }
} }
ErrorBoundary.defaultProps = {
children: <DelayedSpinner />,
};
ErrorBoundary.propTypes = {
children: PropTypes.node,
};

@ -1,4 +1,6 @@
import React from "react"; import React from "react";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import OverlayTrigger from "react-bootstrap/OverlayTrigger"; import OverlayTrigger from "react-bootstrap/OverlayTrigger";
import Popover from "react-bootstrap/Popover"; import Popover from "react-bootstrap/Popover";
@ -6,7 +8,7 @@ import Popover from "react-bootstrap/Popover";
import icon from "./images/info-circle.svg"; import icon from "./images/info-circle.svg";
import "./css/HeaderTooltip.scss"; import "./css/HeaderTooltip.scss";
export default ({ headerTitle, popoverTitle, popoverContent }) => { const HeaderTooltip = ({ headerTitle, popoverTitle, popoverContent }) => {
const { t } = useTranslation("usersTab"); const { t } = useTranslation("usersTab");
const popover = ( const popover = (
@ -25,3 +27,11 @@ export default ({ headerTitle, popoverTitle, popoverContent }) => {
</> </>
); );
}; };
HeaderTooltip.propTypes = {
headerTitle: PropTypes.string.isRequired,
popoverTitle: PropTypes.string.isRequired,
popoverContent: PropTypes.node.isRequired,
};
export default HeaderTooltip;

@ -1,4 +1,6 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import PropTypes from "prop-types";
import classNames from "classnames"; import classNames from "classnames";
import Table from "react-bootstrap/Table"; import Table from "react-bootstrap/Table";
@ -6,7 +8,7 @@ import { useDispatchContext } from "./contexts";
import "./css/ItemTable.scss"; import "./css/ItemTable.scss";
export default ({ tableInstance, itemId }) => { const ItemTable = ({ tableInstance, itemId }) => {
const dispatch = useDispatchContext(); const dispatch = useDispatchContext();
const getHeaderProps = column => const getHeaderProps = column =>
@ -28,7 +30,7 @@ export default ({ tableInstance, itemId }) => {
) : null; ) : null;
const getRowProps = row => { const getRowProps = row => {
const className = itemId && row.original.itemId === itemId ? "ItemTable_row-selected" : undefined; const className = row.original.itemId === itemId ? "ItemTable_row-selected" : undefined;
return row.getRowProps({ className }); return row.getRowProps({ className });
}; };
@ -92,3 +94,15 @@ export default ({ tableInstance, itemId }) => {
</Table> </Table>
); );
}; };
ItemTable.defaultProps = {
itemId: null,
};
ItemTable.propTypes = {
// eslint-disable-next-line react/forbid-prop-types
tableInstance: PropTypes.object.isRequired,
itemId: PropTypes.string,
};
export default ItemTable;

@ -1,4 +1,6 @@
import React from "react"; import React from "react";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import OverlayTrigger from "react-bootstrap/OverlayTrigger"; import OverlayTrigger from "react-bootstrap/OverlayTrigger";
import Popover from "react-bootstrap/Popover"; import Popover from "react-bootstrap/Popover";
@ -6,7 +8,7 @@ import Popover from "react-bootstrap/Popover";
import icon from "./images/info-circle.svg"; import icon from "./images/info-circle.svg";
import "./css/LabelTooltip.scss"; import "./css/LabelTooltip.scss";
export default ({ popoverContent }) => { const LabelTooltip = ({ popoverContent }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const overlay = ( const overlay = (
@ -21,3 +23,9 @@ export default ({ popoverContent }) => {
</OverlayTrigger> </OverlayTrigger>
); );
}; };
LabelTooltip.propTypes = {
popoverContent: PropTypes.node.isRequired,
};
export default LabelTooltip;

@ -1,5 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faUser, faKey } from "@fortawesome/free-solid-svg-icons"; import { faUser, faKey } from "@fortawesome/free-solid-svg-icons";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -23,20 +24,6 @@ const Login = ({ setupClient }) => {
const client = useMatrixClientContext(); const client = useMatrixClientContext();
const onLanguageChange = event => i18n.changeLanguage(event.target.value);
const onUsernameChange = event => setUsername(event.target.value);
const onPasswordChange = event => setPassword(event.target.value);
const onSubmit = event => {
event.preventDefault();
setPendingLogin(prevPendingLogin => {
prevPendingLogin || login();
return true;
});
};
const login = () => const login = () =>
client client
.loginWithPassword(username, password) .loginWithPassword(username, password)
@ -57,6 +44,28 @@ const Login = ({ setupClient }) => {
</Button> </Button>
); );
const onLanguageChange = event => {
i18n.changeLanguage(event.target.value);
};
const onUsernameChange = event => {
setUsername(event.target.value);
};
const onPasswordChange = event => {
setPassword(event.target.value);
};
const onSubmit = event => {
event.preventDefault();
setPendingLogin(prevPendingLogin => {
if (!prevPendingLogin) {
login();
}
return true;
});
};
return ( return (
<Container className="loginForm"> <Container className="loginForm">
<Form.Control className="my-4" as="select" custom value={i18n.language} onChange={onLanguageChange}> <Form.Control className="my-4" as="select" custom value={i18n.language} onChange={onLanguageChange}>

@ -1,10 +1,20 @@
import React from "react"; import React from "react";
import PropTypes from "prop-types";
import Button from "react-bootstrap/Button"; import Button from "react-bootstrap/Button";
import "./css/NewItemButton.scss"; import "./css/NewItemButton.scss";
export default ({ onClick, className, t }) => ( const NewItemButton = ({ onClick, className, t }) => (
<Button variant="primary" {...{ onClick }} title={t("buttonTooltip")}> <Button variant="primary" {...{ onClick }} title={t("buttonTooltip")}>
<span className={`NewItemButton ${className}`}>{t("button")}</span> <span className={`NewItemButton ${className}`}>{t("button")}</span>
</Button> </Button>
); );
NewItemButton.propTypes = {
onClick: PropTypes.func.isRequired,
className: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
};
export default NewItemButton;

@ -1,4 +1,6 @@
import React from "react"; import React from "react";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Button from "react-bootstrap/Button"; import Button from "react-bootstrap/Button";
import Modal from "react-bootstrap/Modal"; import Modal from "react-bootstrap/Modal";
@ -6,7 +8,7 @@ import Spinner from "react-bootstrap/Spinner";
import Alert from "./Alert"; import Alert from "./Alert";
export default ({ feedback, onClick, loading, show, title, onHide, onSave, children }) => { const NewItemModal = ({ feedback, onClick, loading, show, title, onHide, onSave, children }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const footer = feedback ? ( const footer = feedback ? (
@ -39,3 +41,23 @@ export default ({ feedback, onClick, loading, show, title, onHide, onSave, child
</Modal> </Modal>
); );
}; };
NewItemModal.defaultProps = {
feedback: null,
};
NewItemModal.propTypes = {
feedback: PropTypes.shape({
variant: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
}),
onClick: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
show: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired,
onHide: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
children: PropTypes.element.isRequired,
};
export default NewItemModal;

@ -1,4 +1,6 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import PropTypes from "prop-types";
import { Formik } from "formik"; import { Formik } from "formik";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAt } from "@fortawesome/free-solid-svg-icons"; import { faAt } from "@fortawesome/free-solid-svg-icons";
@ -10,7 +12,7 @@ import InputGroup from "react-bootstrap/InputGroup";
import "./css/NewUserForm.scss"; import "./css/NewUserForm.scss";
export default ({ userList, onSubmit, bindSubmitForm, feedback }) => { const NewUserForm = ({ userList, onSubmit, bindSubmitForm, feedback }) => {
const { t } = useTranslation("usersTab"); const { t } = useTranslation("usersTab");
const resetFormRef = useRef(); const resetFormRef = useRef();
@ -85,10 +87,36 @@ export default ({ userList, onSubmit, bindSubmitForm, feedback }) => {
{/* This button is only required to submit the form from a field by pressing the enter key. {/* This button is only required to submit the form from a field by pressing the enter key.
It is therefore hidden. The button actually used is rendered in the parent component. */} It is therefore hidden. The button actually used is rendered in the parent component. */}
<Button type="submit" disabled={feedback} style={{ display: "none" }}></Button> <Button type="submit" disabled={feedback} style={{ display: "none" }} />
</Form> </Form>
); );
}} }}
</Formik> </Formik>
); );
}; };
NewUserForm.defaultProps = {
feedback: null,
};
NewUserForm.propTypes = {
userList: PropTypes.arrayOf(
PropTypes.shape({
userId: PropTypes.string.isRequired,
itemId: PropTypes.string,
displayName: PropTypes.string.isRequired,
emailAddress: PropTypes.string.isRequired,
lastSeen: PropTypes.number,
role: PropTypes.string.isRequired,
creationTs: PropTypes.number,
})
).isRequired,
onSubmit: PropTypes.func.isRequired,
bindSubmitForm: PropTypes.func.isRequired,
feedback: PropTypes.shape({
variant: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
}),
};
export default NewUserForm;

@ -1,66 +0,0 @@
import React, { useRef, useState } from "react";
import { useMutate } from "restful-react";
import { useTranslation } from "react-i18next";
import NewItemModal from "./NewItemModal";
import NewUserForm from "./NewUserForm";
export default ({ modalShow, setModalShow, userList, newUserLocalEcho }) => {
const { t } = useTranslation("usersTab");
const [feedback, setFeedback] = useState(null);
const { mutate: post, loading } = useMutate({
verb: "POST",
path: "watcha_register",
});
const onSubmit = data => {
const payload = makePayload(data);
post(payload)
.then(response => {
const user = makeUser(data);
newUserLocalEcho(user);
setFeedback({ variant: "success", message: t("success") });
})
.catch(error => setFeedback({ variant: "danger", message: t("danger") }));
};
const onHide = () => {
setModalShow(false);
setFeedback(null);
};
const submitFormRef = useRef();
const bindSubmitForm = submitForm => {
submitFormRef.current = submitForm;
};
return (
<NewItemModal
show={modalShow}
title={t("button")}
onSave={() => submitFormRef.current()}
onClick={() => setFeedback(null)}
{...{ feedback, loading, onHide }}
>
<NewUserForm {...{ userList, onSubmit, bindSubmitForm, feedback }} />
</NewItemModal>
);
};
const makePayload = data => ({
admin: data.isSynapseAdministrator,
email: data.emailAddress,
});
const makeUser = data => ({
userId: _genRandomString(),
displayName: "",
emailAddress: data.emailAddress,
lastSeen: null,
role: data.isSynapseAdministrator ? "administrator" : "collaborator",
status: "active",
});
const _genRandomString = () => Math.floor(Math.random() * 1000000).toString();

@ -0,0 +1,91 @@
import React, { useRef, useState } from "react";
import PropTypes from "prop-types";
import { useMutate } from "restful-react";
import { useTranslation } from "react-i18next";
import NewItemModal from "./NewItemModal";
import NewUserForm from "./NewUserForm";
const NewUserModal = ({ modalShow, setModalShow, userList, newUserLocalEcho }) => {
const { t } = useTranslation("usersTab");
const [feedback, setFeedback] = useState(null);
const { mutate: post, loading } = useMutate({
verb: "POST",
path: "watcha_register",
});
const makePayload = data => ({
admin: data.isSynapseAdministrator,
email: data.emailAddress,
});
const genRandomString = () => Math.floor(Math.random() * 1000000).toString();
const makeUser = data => ({
userId: genRandomString(),
displayName: "",
emailAddress: data.emailAddress,
lastSeen: null,
role: data.isSynapseAdministrator ? "administrator" : "collaborator",
status: "active",
});
const onSubmit = data => {
const payload = makePayload(data);
post(payload)
.then(() => {
const user = makeUser(data);
newUserLocalEcho(user);
setFeedback({ variant: "success", message: t("success") });
})
.catch(() => setFeedback({ variant: "danger", message: t("danger") }));
};
const onHide = () => {
setModalShow(false);
setFeedback(null);
};
const submitFormRef = useRef();
const bindSubmitForm = submitForm => {
submitFormRef.current = submitForm;
};
return (
<NewItemModal
show={modalShow}
title={t("button")}
onSave={() => submitFormRef.current()}
onClick={() => setFeedback(null)}
{...{ feedback, loading, onHide }}
>
<NewUserForm {...{ userList, onSubmit, bindSubmitForm, feedback }} />
</NewItemModal>
);
};
NewUserModal.defaultProps = {
userList: null,
};
NewUserModal.propTypes = {
modalShow: PropTypes.bool.isRequired,
setModalShow: PropTypes.func.isRequired,
userList: PropTypes.arrayOf(
PropTypes.shape({
userId: PropTypes.string.isRequired,
itemId: PropTypes.string,
displayName: PropTypes.string.isRequired,
emailAddress: PropTypes.string.isRequired,
lastSeen: PropTypes.number,
role: PropTypes.string.isRequired,
creationTs: PropTypes.number,
})
),
newUserLocalEcho: PropTypes.func.isRequired,
};
export default NewUserModal;

@ -1,10 +0,0 @@
import React from "react";
import "./css/PanelRow.scss";
export default ({ label, value }) => (
<div className="PanelRow">
<div className="PanelRow_label">{label}</div>
<div className="PanelRow_value">{value}</div>
</div>
);

@ -0,0 +1,22 @@
import React from "react";
import PropTypes from "prop-types";
import "./css/PanelRow.scss";
const PanelRow = ({ label, value }) => (
<div className="PanelRow">
<div className="PanelRow_label">{label}</div>
<div className="PanelRow_value">{value}</div>
</div>
);
PanelRow.defaultProps = {
value: "",
};
PanelRow.propTypes = {
label: PropTypes.node.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
export default PanelRow;

@ -1,4 +1,6 @@
import React from "react"; import React from "react";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useMatrixClientContext } from "./contexts"; import { useMatrixClientContext } from "./contexts";
@ -6,7 +8,7 @@ import { useMatrixClientContext } from "./contexts";
import icon from "./images/box-arrow-up-right.svg"; import icon from "./images/box-arrow-up-right.svg";
import "./css/RoomPermalink.scss"; import "./css/RoomPermalink.scss";
export default ({ roomId }) => { const RoomPermalink = ({ roomId }) => {
const { t } = useTranslation("roomsTab"); const { t } = useTranslation("roomsTab");
const client = useMatrixClientContext(); const client = useMatrixClientContext();
@ -26,3 +28,9 @@ export default ({ roomId }) => {
/> />
); );
}; };
RoomPermalink.propTypes = {
roomId: PropTypes.string.isRequired,
};
export default RoomPermalink;

@ -1,4 +1,6 @@
import React from "react"; import React from "react";
import PropTypes from "prop-types";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import Card from "react-bootstrap/Card"; import Card from "react-bootstrap/Card";
@ -7,9 +9,10 @@ import PanelRow from "./PanelRow";
import "./css/DashboardPanel.scss"; import "./css/DashboardPanel.scss";
export default ({ roomsMetrics }) => { const RoomsDashboardPanel = ({ roomsMetrics }) => {
const { t } = useTranslation("dashboardTab"); const { t } = useTranslation("dashboardTab");
// eslint-disable-next-line camelcase
const { regular_room_count, dm_room_count, active_regular_room_count } = roomsMetrics; const { regular_room_count, dm_room_count, active_regular_room_count } = roomsMetrics;
const regularRoomPopoverContent = ["roomsTab:typeHeaderTooltip.content.regularRoom"].map(i18nKey => ( const regularRoomPopoverContent = ["roomsTab:typeHeaderTooltip.content.regularRoom"].map(i18nKey => (
@ -33,9 +36,10 @@ export default ({ roomsMetrics }) => {
<span>{t("roomsPanel.title")}</span> <span>{t("roomsPanel.title")}</span>
</Card.Header> </Card.Header>
<Card.Body className="DashboardPanel_body"> <Card.Body className="DashboardPanel_body">
{/* eslint-disable-next-line camelcase */}
{regular_room_count === 0 && dm_room_count === 0 ? ( {regular_room_count === 0 && dm_room_count === 0 ? (
<div className="DashboardPanel_noRoomMessage"> <div className="DashboardPanel_noRoomMessage">
<Trans t={t} i18nKey={"roomsPanel.noRoomsMessage"} /> <Trans t={t} i18nKey="roomsPanel.noRoomsMessage" />
</div> </div>
) : ( ) : (
<> <>
@ -46,6 +50,7 @@ export default ({ roomsMetrics }) => {
<LabelTooltip popoverContent={regularRoomPopoverContent} /> <LabelTooltip popoverContent={regularRoomPopoverContent} />
</> </>
} }
// eslint-disable-next-line camelcase
value={active_regular_room_count} value={active_regular_room_count}
/> />
<PanelRow <PanelRow
@ -55,6 +60,7 @@ export default ({ roomsMetrics }) => {
<LabelTooltip popoverContent={activeRegularRoomPopoverContent} /> <LabelTooltip popoverContent={activeRegularRoomPopoverContent} />
</> </>
} }
// eslint-disable-next-line camelcase
value={regular_room_count} value={regular_room_count}
/> />
</> </>
@ -63,3 +69,14 @@ export default ({ roomsMetrics }) => {
</Card> </Card>
); );
}; };
RoomsDashboardPanel.propTypes = {
roomsMetrics: PropTypes.shape({
dm_room_count: PropTypes.number.isRequired,
active_dm_room_count: PropTypes.number.isRequired,
regular_room_count: PropTypes.number.isRequired,
active_regular_room_count: PropTypes.number.isRequired,
}).isRequired,
};
export default RoomsDashboardPanel;

@ -1,4 +1,5 @@
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { useMatrixClientContext } from "./contexts"; import { useMatrixClientContext } from "./contexts";
@ -16,6 +17,11 @@ export default () => {
const client = useMatrixClientContext(); const client = useMatrixClientContext();
const getDisplayName = userId => {
const user = client.getUser(userId);
return user && user.displayName;
};
const resolve = data => const resolve = data =>
data.map(item => ({ data.map(item => ({
roomId: item.room_id, roomId: item.room_id,
@ -26,11 +32,6 @@ export default () => {
type: item.type === "dm_room" ? "dmRoom" : "regularRoom", type: item.type === "dm_room" ? "dmRoom" : "regularRoom",
})); }));
const getDisplayName = userId => {
const user = client.getUser(userId);
return user && user.displayName;
};
const requestParams = { const requestParams = {
path: "watcha_room_list", path: "watcha_room_list",
lazy: true, lazy: true,
@ -40,10 +41,10 @@ export default () => {
const typeHeaderPopoverContent = ( const typeHeaderPopoverContent = (
<> <>
<p> <p>
<Trans t={t} i18nKey={"typeHeaderTooltip.content.dmRoom"} /> <Trans t={t} i18nKey="typeHeaderTooltip.content.dmRoom" />
</p> </p>
<p> <p>
<Trans t={t} i18nKey={"typeHeaderTooltip.content.regularRoom"} /> <Trans t={t} i18nKey="typeHeaderTooltip.content.regularRoom" />
</p> </p>
</> </>
); );
@ -52,15 +53,15 @@ export default () => {
<> <>
<p> <p>
<span className="status active" /> <span className="status active" />
<Trans t={t} i18nKey={"statusHeaderTooltip.content.active"} /> <Trans t={t} i18nKey="statusHeaderTooltip.content.active" />
</p> </p>
<p> <p>
<span className="status inactive" /> <span className="status inactive" />
<Trans t={t} i18nKey={"statusHeaderTooltip.content.inactive"} /> <Trans t={t} i18nKey="statusHeaderTooltip.content.inactive" />
</p> </p>
<p> <p>
<span className="status new" /> <span className="status new" />
<Trans t={t} i18nKey={"statusHeaderTooltip.content.new"} /> <Trans t={t} i18nKey="statusHeaderTooltip.content.new" />
</p> </p>
</> </>
); );
@ -71,14 +72,16 @@ export default () => {
Header: t("headers.name"), Header: t("headers.name"),
accessor: "name", accessor: "name",
sortType: compareLowerCase, sortType: compareLowerCase,
Cell: ({ value }) => <span title={value}>{value}</span>, // eslint-disable-next-line react/prop-types
Cell: ({ value }: { value: string }) => <span title={value}>{value}</span>,
}, },
{ {
Header: t("headers.creator"), Header: t("headers.creator"),
accessor: "creator", accessor: "creator",
sortType: compareLowerCase, sortType: compareLowerCase,
disableGlobalFilter: true, disableGlobalFilter: true,
Cell: ({ value }) => <span title={value}>{value}</span>, // eslint-disable-next-line react/prop-types
Cell: ({ value }: { value: string }) => <span title={value}>{value}</span>,
}, },
{ {
Header: t("headers.memberCount"), Header: t("headers.memberCount"),
@ -95,7 +98,8 @@ export default () => {
), ),
accessor: "type", accessor: "type",
disableGlobalFilter: true, disableGlobalFilter: true,
Cell: ({ value }) => t(`type.${value}`), // eslint-disable-next-line react/prop-types
Cell: ({ value }: { value: string }) => t(`type.${value}`),
}, },
{ {
Header: ( Header: (
@ -107,7 +111,8 @@ export default () => {
), ),
accessor: "status", accessor: "status",
disableGlobalFilter: true, disableGlobalFilter: true,
Cell: ({ value }) => <Status status={value} t={t} />, // eslint-disable-next-line react/prop-types
Cell: ({ value }: { value: string }) => <Status status={value} t={t} />,
}, },
], ],
[t] [t]
@ -122,6 +127,7 @@ export default () => {
{ {
id: "permalink", id: "permalink",
Header: "", Header: "",
// eslint-disable-next-line react/prop-types
Cell: ({ row }) => <RoomPermalink roomId={row.original.roomId} />, Cell: ({ row }) => <RoomPermalink roomId={row.original.roomId} />,
}, },
]); ]);

@ -1,10 +1,12 @@
import React from "react"; import React from "react";
import PropTypes from "prop-types";
import Form from "react-bootstrap/Form"; import Form from "react-bootstrap/Form";
import InputGroup from "react-bootstrap/InputGroup"; import InputGroup from "react-bootstrap/InputGroup";
import "./css/SearchBox.scss"; import "./css/SearchBox.scss";
export default ({ tableInstance, t }) => { const SearchBox = ({ tableInstance, t }) => {
const { state, setGlobalFilter } = tableInstance; const { state, setGlobalFilter } = tableInstance;
const onChange = event => setGlobalFilter(event.target.value || undefined); const onChange = event => setGlobalFilter(event.target.value || undefined);
@ -39,3 +41,11 @@ export default ({ tableInstance, t }) => {
</InputGroup> </InputGroup>
); );
}; };
SearchBox.propTypes = {
// eslint-disable-next-line react/forbid-prop-types
tableInstance: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
};
export default SearchBox;

@ -1,5 +0,0 @@
import React from "react";
import "./css/Status.scss";
export default ({ status, t }) => <span className={`Status ${status}`} title={t(`status.${status}`)} />;

@ -0,0 +1,13 @@
import React from "react";
import PropTypes from "prop-types";
import "./css/Status.scss";
const Status = ({ status, t }) => <span className={`Status ${status}`} title={t(`status.${status}`)} />;
Status.propTypes = {
status: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
};
export default Status;

@ -18,7 +18,9 @@ limitations under the License.
let indexedDB; let indexedDB;
try { try {
indexedDB = window.indexedDB; indexedDB = window.indexedDB;
} catch (e) {} } catch (e) {
console.warn(e);
}
let idb = null; let idb = null;
@ -29,10 +31,10 @@ async function idbInit() {
idb = await new Promise((resolve, reject) => { idb = await new Promise((resolve, reject) => {
const request = indexedDB.open("matrix-react-sdk", 1); const request = indexedDB.open("matrix-react-sdk", 1);
request.onerror = reject; request.onerror = reject;
request.onsuccess = event => { request.onsuccess = () => {
resolve(request.result); resolve(request.result);
}; };
request.onupgradeneeded = event => { request.onupgradeneeded = () => {
const db = request.result; const db = request.result;
db.createObjectStore("account"); db.createObjectStore("account");
}; };
@ -50,7 +52,7 @@ export async function idbLoad(table, key) {
const objectStore = txn.objectStore(table); const objectStore = txn.objectStore(table);
const request = objectStore.get(key); const request = objectStore.get(key);
request.onerror = reject; request.onerror = reject;
request.onsuccess = event => { request.onsuccess = () => {
resolve(request.result); resolve(request.result);
}; };
}); });
@ -67,7 +69,7 @@ export async function idbSave(table: string, key: string | string[], data: any):
const objectStore = txn.objectStore(table); const objectStore = txn.objectStore(table);
const request = objectStore.put(data, key); const request = objectStore.put(data, key);
request.onerror = reject; request.onerror = reject;
request.onsuccess = event => { request.onsuccess = () => {
resolve(); resolve();
}; };
}); });
@ -84,7 +86,7 @@ export async function idbDelete(table: string, key: string | string[]): Promise<
const objectStore = txn.objectStore(table); const objectStore = txn.objectStore(table);
const request = objectStore.delete(key); const request = objectStore.delete(key);
request.onerror = reject; request.onerror = reject;
request.onsuccess = event => { request.onsuccess = () => {
resolve(); resolve();
}; };
}); });

@ -1,4 +1,6 @@
import React, { useEffect, useMemo, useRef } from "react"; import React, { useEffect, useMemo, useRef } from "react";
import PropTypes from "prop-types";
import { matchSorter } from "match-sorter"; import { matchSorter } from "match-sorter";
import { useGet } from "restful-react"; import { useGet } from "restful-react";
import { useGlobalFilter, useSortBy, useTable } from "react-table"; import { useGlobalFilter, useSortBy, useTable } from "react-table";
@ -13,7 +15,7 @@ import ItemTable from "./ItemTable";
import "./css/TableTab.scss"; import "./css/TableTab.scss";
export default ({ const TableTab = ({
itemList, itemList,
setItemList, setItemList,
requestParams, requestParams,
@ -44,6 +46,11 @@ export default ({
intervalIdRef.current = setInterval(() => refetchRef.current(), 10000); intervalIdRef.current = setInterval(() => refetchRef.current(), 10000);
}, [data, setItemList]); }, [data, setItemList]);
const fuzzyTextFilterFn = (rows, ids, filterValue) =>
matchSorter(rows, filterValue, {
keys: [row => ids.map(id => row.values[id])],
});
const globalFilter = useMemo(() => fuzzyTextFilterFn, []); const globalFilter = useMemo(() => fuzzyTextFilterFn, []);
const tableInstance = useTable( const tableInstance = useTable(
@ -58,7 +65,7 @@ export default ({
}, },
useGlobalFilter, useGlobalFilter,
useSortBy, useSortBy,
...(plugins || []) ...plugins
); );
const dispatch = useDispatchContext(); const dispatch = useDispatchContext();
@ -84,15 +91,45 @@ export default ({
); );
}; };
const fuzzyTextFilterFn = (rows, ids, filterValue) => TableTab.defaultProps = {
matchSorter(rows, filterValue, { itemList: null,
keys: [row => ids.map(id => row.values[id])], itemId: null,
}); plugins: [],
newItemButton: null,
newItemModal: null,
};
TableTab.propTypes = {
itemList: PropTypes.arrayOf(PropTypes.object),
setItemList: PropTypes.func.isRequired,
itemId: PropTypes.string,
requestParams: PropTypes.shape({
path: PropTypes.string.isRequired,
lazy: PropTypes.bool.isRequired,
resolve: PropTypes.func.isRequired,
}).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
initialState: PropTypes.shape({
sortBy: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string,
desc: PropTypes.bool,
})
),
}).isRequired,
plugins: PropTypes.arrayOf(PropTypes.func),
newItemButton: PropTypes.element,
newItemModal: PropTypes.element,
ns: PropTypes.string.isRequired,
};
export default TableTab;
export function compareLowerCase(rowA, rowB, columnId) { export function compareLowerCase(rowA, rowB, columnId) {
const a = rowA.values[columnId].toLowerCase(); const a = rowA.values[columnId].toLowerCase();
const b = rowB.values[columnId].toLowerCase(); const b = rowB.values[columnId].toLowerCase();
if (a === "") return 1; if (a === "") return 1;
if (b === "") return -1; if (b === "") return -1;
return a === b ? 0 : a > b ? 1 : -1; if (a === b) return 0;
return a > b ? 1 : -1;
} }

@ -1,4 +1,6 @@
import React from "react"; import React from "react";
import PropTypes from "prop-types";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import Card from "react-bootstrap/Card"; import Card from "react-bootstrap/Card";
@ -9,34 +11,37 @@ import PanelRow from "./PanelRow";
import "./css/DashboardPanel.scss"; import "./css/DashboardPanel.scss";
import "./css/UsersDashboardPanel.scss"; import "./css/UsersDashboardPanel.scss";
export default ({ usersMetrics }) => { const UsersDashboardPanel = ({ usersMetrics }) => {
const { t } = useTranslation("dashboardTab"); const { t } = useTranslation("dashboardTab");
// eslint-disable-next-line camelcase
const { administrators_users } = usersMetrics; const { administrators_users } = usersMetrics;
const { administrators, collaborators, partners } = usersMetrics.users_per_role; const { administrators, collaborators, partners } = usersMetrics.users_per_role;
/* eslint-disable camelcase */
const { const {
number_of_users_logged_at_least_once, number_of_users_logged_at_least_once,
number_of_last_month_logged_users, number_of_last_month_logged_users,
number_of_last_week_logged_users, number_of_last_week_logged_users,
} = usersMetrics.connected_users; } = usersMetrics.connected_users;
/* eslint-enable camelcase */
const administratorPopoverContent = ( const administratorPopoverContent = (
<p> <p>
<Trans t={t} i18nKey={"usersTab:roleHeaderTooltip.content.administrator"} /> <Trans t={t} i18nKey="usersTab:roleHeaderTooltip.content.administrator" />
</p> </p>
); );
const collaboratorPopoverContent = ( const collaboratorPopoverContent = (
<p> <p>
<Trans t={t} i18nKey={"usersTab:roleHeaderTooltip.content.collaborator"} /> <Trans t={t} i18nKey="usersTab:roleHeaderTooltip.content.collaborator" />
</p> </p>
); );
const partnerPopoverContent = ( const partnerPopoverContent = (
<p> <p>
<Trans t={t} i18nKey={"usersTab:roleHeaderTooltip.content.partner"} /> <Trans t={t} i18nKey="usersTab:roleHeaderTooltip.content.partner" />
</p> </p>
); );
@ -48,6 +53,7 @@ export default ({ usersMetrics }) => {
<> <>
{t("common:administrators")} {t("common:administrators")}
<LabelTooltip popoverContent={administratorPopoverContent} /> <LabelTooltip popoverContent={administratorPopoverContent} />
{/* eslint-disable-next-line camelcase */}
<AdministratorList administratorList={administrators_users} /> <AdministratorList administratorList={administrators_users} />
</> </>
} }
@ -77,9 +83,11 @@ export default ({ usersMetrics }) => {
const connectedUsersSection = ( const connectedUsersSection = (
<div className="UsersDashboardPanel_panelSection"> <div className="UsersDashboardPanel_panelSection">
<span className="UsersDashboardPanel_panelSectionTitle">{t(`usersPanel.connectedUsers`)}</span> <span className="UsersDashboardPanel_panelSectionTitle">{t(`usersPanel.connectedUsers`)}</span>
{/* eslint-disable camelcase */}
<PanelRow label={t("usersPanel.loggedUsers")} value={number_of_users_logged_at_least_once} /> <PanelRow label={t("usersPanel.loggedUsers")} value={number_of_users_logged_at_least_once} />
<PanelRow label={t("usersPanel.monthlyUsers")} value={number_of_last_month_logged_users} /> <PanelRow label={t("usersPanel.monthlyUsers")} value={number_of_last_month_logged_users} />
<PanelRow label={t("usersPanel.weeklyUsers")} value={number_of_last_week_logged_users} /> <PanelRow label={t("usersPanel.weeklyUsers")} value={number_of_last_week_logged_users} />
{/* eslint-enable camelcase */}
</div> </div>
); );
@ -95,3 +103,27 @@ export default ({ usersMetrics }) => {
</Card> </Card>
); );
}; };
UsersDashboardPanel.propTypes = {
usersMetrics: PropTypes.shape({
administrators_users: PropTypes.arrayOf(
PropTypes.shape({
user_id: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
displayname: PropTypes.string.isRequired,
})
),
users_per_role: PropTypes.shape({
administrators: PropTypes.number.isRequired,
collaborators: PropTypes.number.isRequired,
partners: PropTypes.number.isRequired,
}),
connected_users: PropTypes.shape({
number_of_users_logged_at_least_once: PropTypes.number.isRequired,
number_of_last_month_logged_users: PropTypes.number.isRequired,
number_of_last_week_logged_users: PropTypes.number.isRequired,
}),
}).isRequired,
};
export default UsersDashboardPanel;

@ -1,4 +1,6 @@
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import PropTypes from "prop-types";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import NewItemButton from "./NewItemButton"; import NewItemButton from "./NewItemButton";
@ -10,12 +12,23 @@ import HeaderTooltip from "./HeaderTooltip";
const ns = "usersTab"; const ns = "usersTab";
export default ({ userId }) => { const UsersTab = ({ userId }) => {
const { t } = useTranslation(ns); const { t } = useTranslation(ns);
const [userList, setUserList] = useState(null); const [userList, setUserList] = useState(null);
const [modalShow, setModalShow] = useState(false); const [modalShow, setModalShow] = useState(false);
const resolve = data =>
data.map(item => ({
userId: item.user_id,
itemId: item.user_id,
displayName: item.display_name || "",
emailAddress: item.email_address || "",
lastSeen: item.last_seen || null,
role: item.role,
creationTs: item.creation_ts,
}));
const requestParams = { const requestParams = {
path: "watcha_user_list", path: "watcha_user_list",
lazy: true, lazy: true,
@ -36,13 +49,13 @@ export default ({ userId }) => {
const roleHeaderPopoverContent = ( const roleHeaderPopoverContent = (
<> <>
<p> <p>
<Trans t={t} i18nKey={"roleHeaderTooltip.content.administrator"} /> <Trans t={t} i18nKey="roleHeaderTooltip.content.administrator" />
</p> </p>
<p> <p>
<Trans t={t} i18nKey={"roleHeaderTooltip.content.collaborator"} /> <Trans t={t} i18nKey="roleHeaderTooltip.content.collaborator" />
</p> </p>
<p> <p>
<Trans t={t} i18nKey={"roleHeaderTooltip.content.partner"} /> <Trans t={t} i18nKey="roleHeaderTooltip.content.partner" />
</p> </p>
</> </>
); );
@ -53,19 +66,22 @@ export default ({ userId }) => {
Header: t("headers.displayName"), Header: t("headers.displayName"),
accessor: "displayName", accessor: "displayName",
sortType: compareLowerCase, sortType: compareLowerCase,
Cell: ({ value }) => <span title={value}>{value}</span>, // eslint-disable-next-line react/prop-types
Cell: ({ value }: { value: string }) => <span title={value}>{value}</span>,
}, },
{ {
Header: t("headers.emailAddress"), Header: t("headers.emailAddress"),
accessor: "emailAddress", accessor: "emailAddress",
sortType: compareLowerCase, sortType: compareLowerCase,
Cell: ({ value }) => <span title={value}>{value}</span>, // eslint-disable-next-line react/prop-types
Cell: ({ value }: { value: string }) => <span title={value}>{value}</span>,
}, },
{ {
Header: t("headers.lastSeen"), Header: t("headers.lastSeen"),
accessor: "lastSeen", accessor: "lastSeen",
disableGlobalFilter: true, disableGlobalFilter: true,
Cell: ({ value }) => value && <Date timestamp={value} />, // eslint-disable-next-line react/prop-types
Cell: ({ value }: { value: string }) => (value ? <Date timestamp={value} /> : null),
}, },
{ {
Header: ( Header: (
@ -102,14 +118,12 @@ export default ({ userId }) => {
); );
}; };
const resolve = data => UsersTab.defaultProps = {
data.map(item => ({ userId: null,
userId: item.user_id, };
itemId: item.user_id,
displayName: item.display_name || "", UsersTab.propTypes = {
emailAddress: item.email_address || "", userId: PropTypes.string,
lastSeen: item.last_seen || null, };
role: item.role,
status: item.status, export default UsersTab;
creationTs: item.creation_ts,
}));

@ -8,18 +8,15 @@ import "moment/locale/fr";
const mxLanguageDetector = { const mxLanguageDetector = {
name: "mxLocalSettings", name: "mxLocalSettings",
lookup(options) { lookup() {
const rawValue = localStorage.getItem("mx_local_settings"); const localSettings = localStorage.getItem("mx_local_settings");
if (rawValue) { return localSettings ? JSON.parse(localSettings).language : null;
const localSettings = JSON.parse(rawValue);
return localSettings.language;
}
}, },
cacheUserLanguage(lng, options) { cacheUserLanguage(lng) {
const rawValue = localStorage.getItem("mx_local_settings"); const rawValue = localStorage.getItem("mx_local_settings");
const localSettings = rawValue ? JSON.parse(rawValue) : {}; const localSettings = rawValue ? JSON.parse(rawValue) : {};
localSettings["language"] = lng; localSettings.language = lng;
localStorage.setItem("mx_local_settings", JSON.stringify(localSettings)); localStorage.setItem("mx_local_settings", JSON.stringify(localSettings));
}, },
}; };

Loading…
Cancel
Save