Skip to content

Instantly share code, notes, and snippets.

@RuslanSevrukov
Last active March 2, 2021 13:40
Show Gist options
  • Save RuslanSevrukov/2ce6254f3c9ac31e60ea2ebc92891b14 to your computer and use it in GitHub Desktop.
Save RuslanSevrukov/2ce6254f3c9ac31e60ea2ebc92891b14 to your computer and use it in GitHub Desktop.
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import { observer } from "mobx-react-lite";
import { get as mobxGet } from "mobx";
import filter from "lodash/fp/filter";
import get from "lodash/fp/get";
import getOr from "lodash/fp/getOr";
import groupBy from "lodash/fp/groupBy";
import mapValues from "lodash/fp/mapValues";
import max from "lodash/fp/max";
import flatten from "lodash/fp/flatten";
import pipe from "lodash/fp/pipe";
import sum from "lodash/fp/sum";
import sumBy from "lodash/fp/sumBy";
import toInteger from "lodash/fp/toInteger";
import values from "lodash/fp/values";
import MatrixCell from "./MatrixCell";
import MatrixRow from "./MatrixRow";
import ExportMenu from "../../ExportMenu";
import LoadingChart from "../LoadingChart";
import ErrorMessage from "../../../ErrorMessage";
import {
matrixViolationTypeToLabel,
violationCountLabel,
} from "../../../../utils/transforms/labels";
import { translate } from "../../../../views/tenants/utils/i18nSettings";
import { useContainerHeight } from "../../../../hooks/useContainerHeight";
import { useRootStore } from "../../../../StoreProvider";
import {
VIOLATION_TYPE,
REPORTED_VIOLATIONS,
} from "../../../../constants/filterTypes";
import "./Matrix.scss";
const TITLE = "Admissions by violation history (in year prior to their last reported violation)";
const VIOLATION_COUNTS = ["1", "2", "3", "4", "5", "6", "7", "8"];
const getInteger = (field) => pipe(get(field), toInteger);
const sumByInteger = (field) => sumBy(getInteger(field));
const sumRow = pipe(values, sum);
const Matrix = ({ timeDescription }) => {
const { dataStore, filters, filtersStore } = useRootStore();
const { filterOptions } = filtersStore;
const store = dataStore.matrixStore;
const { containerHeight, containerRef } = useContainerHeight();
const violationTypes = translate("violationTypes");
if (store.isLoading) {
return <LoadingChart containerHeight={containerHeight} />;
}
if (store.isError) {
return <ErrorMessage />;
}
const updateFilters = (updatedFilters) => {
filtersStore.setFilters(updatedFilters);
};
const isFiltered = mobxGet(filters, VIOLATION_TYPE) || mobxGet(filters, REPORTED_VIOLATIONS);
const filteredData = pipe(
filter((data) => violationTypes.includes(data.violation_type))
)(store.filteredData);
const dataMatrix = pipe(
groupBy("violation_type"),
mapValues(
pipe(
groupBy("reported_violations"),
mapValues(sumByInteger("total_revocations"))
)
)
)(filteredData);
if (!dataMatrix) {
return null;
}
const maxRevocations = pipe(
() =>
violationTypes.map((rowLabel) =>
VIOLATION_COUNTS.map((columnLabel) =>
getOr(0, [rowLabel, columnLabel], dataMatrix)
)
),
flatten,
max
)();
const violationsSum = sumByInteger("total_revocations")(filteredData);
const reportedViolationsSum = pipe(
(count) =>
filter((data) => data.reported_violations === count, filteredData),
sumByInteger("total_revocations")
);
const isSelected = (violationType, reportedViolations) =>
mobxGet(filters, VIOLATION_TYPE) === violationType &&
mobxGet(filters, REPORTED_VIOLATIONS) === reportedViolations;
const toggleFilter = (violationType, reportedViolations) => {
if (isSelected(violationType, reportedViolations)) {
updateFilters({
violationType: filterOptions[VIOLATION_TYPE].defaultValue,
reportedViolations: filterOptions[REPORTED_VIOLATIONS].defaultValue,
});
} else {
updateFilters({ violationType, reportedViolations });
}
};
const exportableMatrixData = violationTypes.map((rowLabel) => ({
label: matrixViolationTypeToLabel[rowLabel],
data: VIOLATION_COUNTS.map((columnLabel) =>
getOr(0, [rowLabel, columnLabel], dataMatrix)
),
}));
return (
<div ref={containerRef} className="Matrix">
<h4 className="Matrix__title">
{TITLE}
<ExportMenu
chartId={`${translate("revocation")}Matrix`}
regularElement
datasets={exportableMatrixData}
labels={VIOLATION_COUNTS.map(violationCountLabel)}
metricTitle={TITLE}
timeWindowDescription={timeDescription}
fixLabelsInColumns
dataExportLabel="Violations"
/>
</h4>
<h6 className="Matrix__dates">{timeDescription}</h6>
<div className="Matrix__x-label">
# of {translate("violationReports")}
</div>
<div id="revocationMatrix" className="Matrix__chart-container">
<div className="Matrix__y-label" data-html2canvas-ignore>
Most severe violation reported
</div>
<div
className={cx("Matrix__matrix", {
"Matrix__matrix--is-filtered": isFiltered,
})}
>
<div className="Matrix__violation-counts">
<span className="Matrix__empty-cell" />
{VIOLATION_COUNTS.map((count, i) => (
<span key={i} className="Matrix__violation-column">
{violationCountLabel(count)}
</span>
))}
<span
className={cx(
"Matrix__violation-sum-column",
"Matrix__top-right-total"
)}
>
Total
</span>
</div>
{violationTypes.map((violationType, i) => (
<MatrixRow
key={i}
violationType={violationType}
sum={sumRow(dataMatrix[violationType])}
onClick={() => toggleFilter(violationType, "All")}
>
{VIOLATION_COUNTS.map((violationCount, j) => (
<MatrixCell
key={j}
count={getOr(0, [violationType, violationCount], dataMatrix)}
maxCount={maxRevocations}
violationType={violationType}
reportedViolations={violationCount}
onClick={() => toggleFilter(violationType, violationCount)}
/>
))}
</MatrixRow>
))}
<div className="Matrix__violation-sum-row">
<span className="Matrix__empty-cell" />
{VIOLATION_COUNTS.map((count, i) => (
<span
key={i}
className={cx(
"Matrix__violation-column",
"Matrix__violation-sum"
)}
>
{reportedViolationsSum(count)}
</span>
))}
<span
className={cx(
"Matrix__violation-sum-column",
"Matrix__violation-sum",
"Matrix__bottom-right-total"
)}
>
{violationsSum}
</span>
</div>
</div>
</div>
</div>
);
};
Matrix.propTypes = {
timeDescription: PropTypes.string.isRequired,
};
export default observer(Matrix);
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import { get } from "mobx";
import { useRootStore } from "../../../../StoreProvider";
import {
VIOLATION_TYPE,
REPORTED_VIOLATIONS,
} from "../../../../constants/filterTypes";
import { COLORS } from "../../../../assets/scripts/constants/colors";
const minRadius = 25;
const maxRadius = 50;
const MatrixCell = ({
count,
maxCount,
violationType,
reportedViolations,
onClick,
}) => {
const { filters } = useRootStore();
const ratio = maxCount > 0 ? count / maxCount : 0;
const radius = Math.max(minRadius, Math.ceil(ratio * maxRadius) + 15);
const containerStyle = {
position: "relative",
zIndex: 1,
background: "white",
display: "inline-block",
width: radius,
height: radius,
lineHeight: `${radius}px`,
};
const cellStyle = {
// lantern-dark-blue with opacity
background:
ratio === 0 ? COLORS.white : `rgba(0, 44, 66, ${Math.max(ratio, 0.05)})`,
width: "100%",
height: "100%",
borderRadius: Math.ceil(radius / 2),
color: ratio >= 0.5 ? COLORS.white : COLORS["lantern-dark-blue"],
};
const isCellSelected =
(get(filters, VIOLATION_TYPE) === "All" ||
get(filters, VIOLATION_TYPE) === violationType) &&
(get(filters, REPORTED_VIOLATIONS) === "All" ||
get(filters, REPORTED_VIOLATIONS) === reportedViolations);
return (
<div className="Matrix__cell">
<div style={containerStyle}>
<button
type="button"
className={cx("Matrix__total-revocations", {
"is-selected": isCellSelected,
})}
onClick={onClick}
style={cellStyle}
>
{count}
</button>
</div>
</div>
);
};
MatrixCell.propTypes = {
count: PropTypes.number.isRequired,
maxCount: PropTypes.number.isRequired,
violationType: PropTypes.string.isRequired,
reportedViolations: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
};
export default MatrixCell;
import { makeAutoObservable, when } from "mobx";
import filter from "lodash/fp/filter";
import identity from "lodash/fp/identity";
import map from "lodash/fp/map";
import pipe from "lodash/fp/pipe";
import sortBy from "lodash/fp/sortBy";
import uniq from "lodash/fp/uniq";
import { getAvailableStateCodes, doesUserHaveAccess } from "./utils/user";
import { LANTERN_TENANTS } from "../views/tenants/utils/lanternTenants";
export const CURRENT_TENANT_IN_SESSION = "adminUserCurrentTenantInSession";
/*
* Returns the current state that should be viewed. This is retrieved from
* the sessionStorage cache if already set. Otherwise, picks the first available state in ABC order.
*/
function getTenantIdFromUser(user) {
const fromStorage = sessionStorage.getItem(CURRENT_TENANT_IN_SESSION);
if (user) {
const availableStateCodes = getAvailableStateCodes(user);
if (fromStorage && doesUserHaveAccess(user, fromStorage)) {
return fromStorage;
}
return availableStateCodes[0];
}
return fromStorage;
}
export default class TenantStore {
rootStore;
currentTenantId = null;
districts = [];
districtsIsLoading = true;
constructor({ rootStore }) {
makeAutoObservable(this);
this.rootStore = rootStore;
when(
() => !this.rootStore.userStore.userIsLoading,
() => this.setCurrentTenantId(getTenantIdFromUser(this.rootStore.user))
);
}
setCurrentTenantId(tenantId) {
this.currentTenantId = tenantId;
sessionStorage.setItem(CURRENT_TENANT_IN_SESSION, tenantId);
this.districtsIsLoading = true;
}
setDistricts(apiData) {
if (apiData) {
const data = apiData.slice();
this.districts = pipe(
map("district"),
filter((d) => d.toLowerCase() !== "all"),
uniq,
sortBy(identity)
)(data);
this.districtsIsLoading = false;
}
}
get isLanternTenant() {
return LANTERN_TENANTS.includes(this.currentTenantId);
}
}
import createAuth0Client, { Auth0ClientOptions } from "@auth0/auth0-spa-js";
import { makeAutoObservable, runInAction, autorun, flow } from "mobx";
import qs from "qs";
import * as Sentry from "@sentry/react";
import { ERROR_MESSAGES } from "../constants/errorMessages";
import type RootStore from "./RootStore";
import {
getUserStateCode,
getStateNameForCode,
getAvailableStateCodes,
} from "./utils/user";
import { callRestrictedAccessApi } from "../api/metrics/metricsClient";
import isDemoMode from "../utils/authentication/demoMode";
import { getDemoUser } from "../utils/authentication/viewAuthentication";
type ConstructorProps = {
authSettings?: Auth0ClientOptions;
rootStore?: RootStore;
};
type RestrictedAccessEmail = {
// eslint-disable-next-line camelcase
restricted_user_email: string;
// eslint-disable-next-line camelcase
allowed_level_1_supervision_location_ids: string;
};
/**
* Reactive wrapper around Auth0 client.
* Call `authorize` to retrieve credentials or start login flow.
*
* @example
*
* ```js
* const store = new UserStore({ authSettings: { domain, client_id, redirect_uri } });
* if (!store.isAuthorized) {
* await store.authorize();
* // this may trigger a redirect to the Auth0 login domain;
* // if we're still here and user has successfully logged in,
* // store.isAuthorized should now be true.
* }
* ```
*/
export default class UserStore {
authError?: Error;
readonly authSettings?: Auth0ClientOptions;
isAuthorized: boolean;
userIsLoading: boolean;
// TODO TS create user type
user: any;
getTokenSilently?: () => void;
logout?: () => void;
restrictedDistrict?: string;
restrictedDistrictIsLoading: boolean;
readonly rootStore?: RootStore;
constructor({ authSettings, rootStore }: ConstructorProps) {
makeAutoObservable(this, {
rootStore: false,
authSettings: false,
});
this.authSettings = authSettings;
this.rootStore = rootStore;
this.isAuthorized = false;
this.userIsLoading = true;
this.restrictedDistrictIsLoading = true;
autorun(() => {
if (!this.userIsLoading && this.rootStore?.currentTenantId) {
this.fetchRestrictedDistrictData(this.rootStore?.currentTenantId);
}
});
}
/**
* If user already has a valid Auth0 credential, this method will retrieve it
* and update class properties accordingly. If not, user will be redirected
* to the Auth0 login domain for fresh authentication.
* Returns an Error if Auth0 configuration is not present.
*/
async authorize(): Promise<void> {
if (isDemoMode()) {
this.isAuthorized = true;
this.userIsLoading = false;
this.user = getDemoUser();
this.getTokenSilently = () => "";
return;
}
if (!this.authSettings) {
this.authError = new Error(ERROR_MESSAGES.auth0Configuration);
return;
}
try {
const auth0 = await createAuth0Client(this.authSettings);
const urlQuery = qs.parse(window.location.search, {
ignoreQueryPrefix: true,
});
if (urlQuery.code && urlQuery.state) {
const { appState } = await auth0.handleRedirectCallback();
// auth0 params are single-use, must be removed from history or they can cause errors
let replacementUrl;
if (appState && appState.targetUrl) {
replacementUrl = appState.targetUrl;
} else {
// strip away all query params just to be safe
replacementUrl = `${window.location.origin}${window.location.pathname}`;
}
window.history.replaceState({}, document.title, replacementUrl);
}
if (await auth0.isAuthenticated()) {
const user = await auth0.getUser();
runInAction(() => {
this.userIsLoading = false;
if (user && user.email_verified) {
this.user = user;
this.getTokenSilently = (...p: any) => auth0.getTokenSilently(...p);
this.logout = (...p: any) => auth0.logout(...p);
this.isAuthorized = true;
} else {
this.isAuthorized = false;
}
});
} else {
auth0.loginWithRedirect({
appState: { targetUrl: window.location.href },
});
}
} catch (error) {
this.authError = error;
}
}
/**
* Returns the list of states which are accessible to users to view data for.
*
*/
get availableStateCodes(): Array<string> {
return getAvailableStateCodes(this.user);
}
/**
* Returns the human-readable state name for the authorized state code for the given usere.
*/
get stateName(): string {
return getStateNameForCode(this.stateCode);
}
/**
* Returns the state code of the authorized state for the given user.
* For Recidiviz users or users in demo mode, this will be 'recidiviz'.
*/
get stateCode(): string {
return getUserStateCode(this.user);
}
fetchRestrictedDistrictData = flow(function* (
this: UserStore,
tenantId: string
) {
if (!this.rootStore?.tenantStore.isLanternTenant) {
this.restrictedDistrictIsLoading = false;
return;
}
const file = "supervision_location_restricted_access_emails";
const endpoint = `${tenantId}/restrictedAccess`;
try {
this.restrictedDistrict = undefined;
const responseData = yield callRestrictedAccessApi(
endpoint,
this.user.email,
this.getTokenSilently
);
this.setRestrictedDistrict(responseData[file]);
this.restrictedDistrictIsLoading = false;
} catch (error) {
Sentry.captureException(error, {
tags: {
tenantId,
endpoint,
availableStateCodes: this.availableStateCodes.join(","),
},
});
this.authError = new Error(ERROR_MESSAGES.unauthorized);
this.restrictedDistrictIsLoading = false;
}
});
setRestrictedDistrict(restrictedEmail: RestrictedAccessEmail): void {
this.restrictedDistrict =
restrictedEmail &&
restrictedEmail.allowed_level_1_supervision_location_ids;
this.verifyRestrictedDistrict();
}
verifyRestrictedDistrict(): void {
if (
this.restrictedDistrict &&
!this.rootStore?.tenantStore.districtsIsLoading &&
!this.rootStore?.tenantStore.districts.includes(this.restrictedDistrict)
) {
const authError = new Error(ERROR_MESSAGES.unauthorized);
Sentry.captureException(authError, {
tags: {
restrictedDistrict: this.restrictedDistrict,
},
});
this.authError = authError;
this.restrictedDistrictIsLoading = false;
this.restrictedDistrict = undefined;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment